diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 608565fdc..9ecd27cc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,17 +14,18 @@ on: - cron: '0 5 * * 4' concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}" cancel-in-progress: true - +permissions: + repository-projects: read jobs: build_linux: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-18.04, ubuntu-20.04, ubuntu-22.04 ] - python-version: ["3.8", "3.9", "3.10"] + os: [ ubuntu-20.04, ubuntu-22.04 ] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -76,6 +77,17 @@ jobs: # Allow failure for coveralls coveralls || true + - name: Check for repository changes + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "Repository is dirty, changes detected:" + git status + git diff + exit 1 + else + echo "Repository is clean, no changes detected." + fi + - name: Backtesting (multi) run: | cp config_examples/config_bittrex.example.json config.json @@ -90,14 +102,14 @@ jobs: freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - - name: Flake8 - run: | - flake8 - - name: Sort imports (isort) run: | isort --check . + - name: Run Ruff + run: | + ruff check --format=github . + - name: Mypy run: | mypy freqtrade scripts tests @@ -115,7 +127,7 @@ jobs: strategy: matrix: os: [ macos-latest ] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -173,6 +185,17 @@ jobs: run: | pytest --random-order + - name: Check for repository changes + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "Repository is dirty, changes detected:" + git status + git diff + exit 1 + else + echo "Repository is clean, no changes detected." + fi + - name: Backtesting run: | cp config_examples/config_bittrex.example.json config.json @@ -186,14 +209,14 @@ jobs: freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - - name: Flake8 - run: | - flake8 - - name: Sort imports (isort) run: | isort --check . + - name: Run Ruff + run: | + ruff check --format=github . + - name: Mypy run: | mypy freqtrade scripts @@ -212,7 +235,7 @@ jobs: strategy: matrix: os: [ windows-latest ] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 @@ -236,6 +259,18 @@ jobs: run: | pytest --random-order + - name: Check for repository changes + run: | + if (git status --porcelain) { + Write-Host "Repository is dirty, changes detected:" + git status + git diff + exit 1 + } + else { + Write-Host "Repository is clean, no changes detected." + } + - name: Backtesting run: | cp config_examples/config_bittrex.example.json config.json @@ -248,9 +283,9 @@ jobs: freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - - name: Flake8 + - name: Run Ruff run: | - flake8 + ruff check --format=github . - name: Mypy run: | @@ -301,7 +336,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.11" - name: Documentation build run: | @@ -321,7 +356,6 @@ jobs: build_linux_online: # Run pytest with "live" checks runs-on: ubuntu-22.04 - # permissions: steps: - uses: actions/checkout@v3 @@ -360,6 +394,8 @@ jobs: pip install -e . - name: Tests incl. ccxt compatibility tests + env: + CI_WEB_PROXY: http://152.67.78.211:13128 run: | pytest --random-order --cov=freqtrade --cov-config=.coveragerc --longrun @@ -423,7 +459,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.8.6 if: (github.event_name == 'release') with: user: __token__ @@ -431,7 +467,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.8.6 if: (github.event_name == 'release') with: user: __token__ @@ -464,12 +500,13 @@ jobs: - name: Build and test and push docker images env: - IMAGE_NAME: freqtradeorg/freqtrade BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} run: | build_helpers/publish_docker_multi.sh deploy_arm: + permissions: + packages: write needs: [ deploy ] # Only run on 64bit machines runs-on: [self-hosted, linux, ARM64] @@ -492,8 +529,9 @@ jobs: - name: Build and test and push docker images env: - IMAGE_NAME: freqtradeorg/freqtrade BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} + GHCR_USERNAME: ${{ github.actor }} + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | build_helpers/publish_docker_arm64.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 306e4bbda..4be298d7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,33 +2,40 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pycqa/flake8 - rev: "4.0.1" + rev: "6.0.0" hooks: - id: flake8 # stages: [push] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v0.942" + rev: "v1.0.1" hooks: - id: mypy exclude: build_helpers additional_dependencies: - - types-cachetools==5.2.1 + - types-cachetools==5.3.0.5 - types-filelock==3.2.7 - - types-requests==2.28.11.7 - - types-tabulate==0.9.0.0 - - types-python-dateutil==2.8.19.5 + - types-requests==2.30.0.0 + - types-tabulate==0.9.0.2 + - types-python-dateutil==2.8.19.13 + - SQLAlchemy==2.0.15 # stages: [push] - repo: https://github.com/pycqa/isort - rev: "5.10.1" + rev: "5.12.0" hooks: - id: isort name: isort (python) # stages: [push] + - repo: https://github.com/charliermarsh/ruff-pre-commit + # Ruff version. + rev: 'v0.0.263' + hooks: + - id: ruff + - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v4.4.0 hooks: - id: end-of-file-fixer exclude: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4e0bc024..040aae39c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,16 +45,17 @@ pytest tests/test_.py::test_ ### 2. Test if your code is PEP8 compliant -#### Run Flake8 +#### Run Ruff ```bash -flake8 freqtrade tests scripts +ruff . ``` -We receive a lot of code that fails the `flake8` checks. +We receive a lot of code that fails the `ruff` checks. To help with that, we encourage you to install the git pre-commit -hook that will warn you when you try to commit code that fails these checks. -Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html). +hook that will warn you when you try to commit code that fails these checks. + +you can manually run pre-commit with `pre-commit run -a`. ##### Additional styles applied diff --git a/Dockerfile b/Dockerfile index b3e5d5e88..d3890a25b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.7-slim-bullseye as base +FROM python:3.10.11-slim-bullseye as base # Setup env ENV LANG C.UTF-8 @@ -25,7 +25,7 @@ FROM base as python-deps RUN apt-get update \ && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get clean \ - && pip install --upgrade pip + && pip install --upgrade pip wheel # Install TA-lib COPY build_helpers/* /tmp/ diff --git a/README.md b/README.md index 2ab62793d..57c4e3a52 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [Binance](https://www.binance.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [OKX](https://okx.com/) +- [X] [Bybit](https://bybit.com/) Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in. @@ -164,6 +165,10 @@ first. If it hasn't been reported, please ensure you follow the template guide so that the team can assist you as quickly as possible. +For every [issue](https://github.com/freqtrade/freqtrade/issues/new/choose) created, kindly follow up and mark satisfaction or reminder to close issue when equilibrium ground is reached. + +--Maintain github's [community policy](https://docs.github.com/en/site-policy/github-terms/github-community-code-of-conduct)-- + ### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) Have you a great idea to improve the bot you want to share? Please, @@ -205,6 +210,6 @@ To run this bot we recommend you a cloud instance with a minimum of: - [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/) - [pip](https://pip.pypa.io/en/stable/installing/) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) +- [TA-Lib](https://ta-lib.github.io/ta-lib-python/) - [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) - [Docker](https://www.docker.com/products/docker) (Recommended) diff --git a/build_helpers/TA_Lib-0.4.25-cp310-cp310-win_amd64.whl b/build_helpers/TA_Lib-0.4.25-cp310-cp310-win_amd64.whl deleted file mode 100644 index c6435da0d..000000000 Binary files a/build_helpers/TA_Lib-0.4.25-cp310-cp310-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.25-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.25-cp38-cp38-win_amd64.whl deleted file mode 100644 index f2806db80..000000000 Binary files a/build_helpers/TA_Lib-0.4.25-cp38-cp38-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.25-cp39-cp39-win_amd64.whl b/build_helpers/TA_Lib-0.4.25-cp39-cp39-win_amd64.whl deleted file mode 100644 index 0d4ceb3b4..000000000 Binary files a/build_helpers/TA_Lib-0.4.25-cp39-cp39-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl new file mode 100644 index 000000000..466455a8c Binary files /dev/null and b/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl new file mode 100644 index 000000000..16f4f411a Binary files /dev/null and b/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl new file mode 100644 index 000000000..324502fb8 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl new file mode 100644 index 000000000..389403041 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl differ diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index 079d578b4..005d9abca 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -8,8 +8,8 @@ if [ -n "$2" ] || [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then tar zxvf ta-lib-0.4.0-src.tar.gz cd ta-lib \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ - && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \ - && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \ + && curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.guess' -o config.guess \ + && curl 'https://raw.githubusercontent.com/gcc-mirror/gcc/master/config.sub' -o config.sub \ && ./configure --prefix=${INSTALL_LOC}/ \ && make if [ $? -ne 0 ]; then diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 461726a03..2fc21d317 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -6,13 +6,16 @@ python -m pip install --upgrade pip wheel $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" if ($pyv -eq '3.8') { - pip install build_helpers\TA_Lib-0.4.25-cp38-cp38-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.26-cp38-cp38-win_amd64.whl } if ($pyv -eq '3.9') { - pip install build_helpers\TA_Lib-0.4.25-cp39-cp39-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.26-cp39-cp39-win_amd64.whl } if ($pyv -eq '3.10') { - pip install build_helpers\TA_Lib-0.4.25-cp310-cp310-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.26-cp310-cp310-win_amd64.whl +} +if ($pyv -eq '3.11') { + pip install build_helpers\TA_Lib-0.4.26-cp311-cp311-win_amd64.whl } pip install -r requirements-dev.txt pip install -e . diff --git a/build_helpers/pre_commit_update.py b/build_helpers/pre_commit_update.py index 8724d8ade..e6b47d100 100644 --- a/build_helpers/pre_commit_update.py +++ b/build_helpers/pre_commit_update.py @@ -8,12 +8,17 @@ import yaml pre_commit_file = Path('.pre-commit-config.yaml') require_dev = Path('requirements-dev.txt') +require = Path('requirements.txt') with require_dev.open('r') as rfile: requirements = rfile.readlines() +with require.open('r') as rfile: + requirements.extend(rfile.readlines()) + # Extract types only -type_reqs = [r.strip('\n') for r in requirements if r.startswith('types-')] +type_reqs = [r.strip('\n') for r in requirements if r.startswith( + 'types-') or r.startswith('SQLAlchemy')] with pre_commit_file.open('r') as file: f = yaml.load(file, Loader=yaml.FullLoader) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 071eb0fa2..8f0de2cc9 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -3,18 +3,22 @@ # Use BuildKit, otherwise building on ARM fails export DOCKER_BUILDKIT=1 +IMAGE_NAME=freqtradeorg/freqtrade +CACHE_IMAGE=freqtradeorg/freqtrade_cache +GHCR_IMAGE_NAME=ghcr.io/freqtrade/freqtrade + # Replace / with _ to create a valid tag TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot TAG_FREQAI=${TAG}_freqai TAG_FREQAI_RL=${TAG_FREQAI}rl +TAG_FREQAI_TORCH=${TAG_FREQAI}torch TAG_PI="${TAG}_pi" TAG_ARM=${TAG}_arm TAG_PLOT_ARM=${TAG_PLOT}_arm TAG_FREQAI_ARM=${TAG_FREQAI}_arm TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm -CACHE_IMAGE=freqtradeorg/freqtrade_cache echo "Running for ${TAG}" @@ -38,13 +42,13 @@ if [ $? -ne 0 ]; then echo "failed building multiarch images" return 1 fi + +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl . + # Tag image for upload and next build step docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM - -docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . -docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai . -docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl . - docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM @@ -59,7 +63,6 @@ fi docker images -# docker push ${IMAGE_NAME} docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM @@ -70,25 +73,47 @@ docker push ${CACHE_IMAGE}:$TAG_ARM # Otherwise installation might fail. echo "create manifests" -docker manifest create --amend ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} +docker manifest create ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} docker manifest push -p ${IMAGE_NAME}:${TAG} -docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT} +docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT} -docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} ${CACHE_IMAGE}:${TAG_FREQAI} +docker manifest create ${IMAGE_NAME}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI} ${CACHE_IMAGE}:${TAG_FREQAI_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI} -docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} ${CACHE_IMAGE}:${TAG_FREQAI_RL} +docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} +# Create special Torch tag - which is identical to the RL tag. +docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_TORCH} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} +docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_TORCH} + +# copy images to ghcr.io + +alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" +mkdir .crane +chmod a+rwx .crane + +echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin + +crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} +crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_TORCH} +crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} +crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} +crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} + # Tag as latest for develop builds if [ "${TAG}" = "develop" ]; then + echo 'Tagging image as latest' docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} docker manifest push -p ${IMAGE_NAME}:latest + + crane copy ${IMAGE_NAME}:latest ${GHCR_IMAGE_NAME}:latest fi docker images +rm -rf .crane # Cleanup old images from arm64 node. docker image prune -a --force --filter "until=24h" diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index a608c1282..72b20ac5d 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -2,6 +2,8 @@ # The below assumes a correctly setup docker buildx environment +IMAGE_NAME=freqtradeorg/freqtrade +CACHE_IMAGE=freqtradeorg/freqtrade_cache # Replace / with _ to create a valid tag TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot @@ -11,7 +13,6 @@ TAG_PI="${TAG}_pi" PI_PLATFORM="linux/arm/v7" echo "Running for ${TAG}" -CACHE_IMAGE=freqtradeorg/freqtrade_cache CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache # Add commit and commit_message to docker container @@ -26,7 +27,10 @@ if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then --cache-to=type=registry,ref=${CACHE_TAG} \ -f docker/Dockerfile.armhf \ --platform ${PI_PLATFORM} \ - -t ${IMAGE_NAME}:${TAG_PI} --push . + -t ${IMAGE_NAME}:${TAG_PI} \ + --push \ + --provenance=false \ + . else echo "event ${GITHUB_EVENT_NAME}: building with cache" # Build regular image @@ -35,12 +39,16 @@ else # Pull last build to avoid rebuilding the whole image # docker pull --platform ${PI_PLATFORM} ${IMAGE_NAME}:${TAG} + # disable provenance due to https://github.com/docker/buildx/issues/1509 docker buildx build \ --cache-from=type=registry,ref=${CACHE_TAG} \ --cache-to=type=registry,ref=${CACHE_TAG} \ -f docker/Dockerfile.armhf \ --platform ${PI_PLATFORM} \ - -t ${IMAGE_NAME}:${TAG_PI} --push . + -t ${IMAGE_NAME}:${TAG_PI} \ + --push \ + --provenance=false \ + . fi if [ $? -ne 0 ]; then @@ -50,9 +58,9 @@ fi # Tag image for upload and next build step docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG -docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . -docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai . -docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl . docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI @@ -68,12 +76,10 @@ fi docker images -docker push ${CACHE_IMAGE} +docker push ${CACHE_IMAGE}:$TAG docker push ${CACHE_IMAGE}:$TAG_PLOT docker push ${CACHE_IMAGE}:$TAG_FREQAI docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL -docker push ${CACHE_IMAGE}:$TAG - docker images diff --git a/build_helpers/pyarrow-10.0.0-cp39-cp39-linux_armv7l.whl b/build_helpers/pyarrow-12.0.0-cp39-cp39-linux_armv7l.whl similarity index 55% rename from build_helpers/pyarrow-10.0.0-cp39-cp39-linux_armv7l.whl rename to build_helpers/pyarrow-12.0.0-cp39-cp39-linux_armv7l.whl index a6c879cf5..2a8d1ff51 100644 Binary files a/build_helpers/pyarrow-10.0.0-cp39-cp39-linux_armv7l.whl and b/build_helpers/pyarrow-12.0.0-cp39-cp39-linux_armv7l.whl differ diff --git a/config_examples/config_binance.example.json b/config_examples/config_binance.example.json index 3e99bd114..7968bdedc 100644 --- a/config_examples/config_binance.example.json +++ b/config_examples/config_binance.example.json @@ -59,20 +59,6 @@ "pairlists": [ {"method": "StaticPairList"} ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, "telegram": { "enabled": false, "token": "your_telegram_token", diff --git a/config_examples/config_bittrex.example.json b/config_examples/config_bittrex.example.json index a0a5071dd..3be5ba092 100644 --- a/config_examples/config_bittrex.example.json +++ b/config_examples/config_bittrex.example.json @@ -56,20 +56,6 @@ "pairlists": [ {"method": "StaticPairList"} ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, "telegram": { "enabled": false, "token": "your_telegram_token", diff --git a/config_examples/config_freqai.example.json b/config_examples/config_freqai.example.json index dfd54b3d9..65a93379e 100644 --- a/config_examples/config_freqai.example.json +++ b/config_examples/config_freqai.example.json @@ -21,8 +21,8 @@ "ccxt_config": {}, "ccxt_async_config": {}, "pair_whitelist": [ - "1INCH/USDT", - "ALGO/USDT" + "1INCH/USDT:USDT", + "ALGO/USDT:USDT" ], "pair_blacklist": [] }, @@ -48,7 +48,7 @@ ], "freqai": { "enabled": true, - "purge_old_models": true, + "purge_old_models": 2, "train_period_days": 15, "backtest_period_days": 7, "live_retrain_hours": 0, @@ -60,8 +60,8 @@ "1h" ], "include_corr_pairlist": [ - "BTC/USDT", - "ETH/USDT" + "BTC/USDT:USDT", + "ETH/USDT:USDT" ], "label_period_candles": 20, "include_shifted_candles": 2, diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index b60957b58..64e5b76ea 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -60,6 +60,7 @@ "force_entry": "market", "stoploss": "market", "stoploss_on_exchange": false, + "stoploss_price_type": "last", "stoploss_on_exchange_interval": 60, "stoploss_on_exchange_limit_ratio": 0.99 }, diff --git a/config_examples/config_kraken.example.json b/config_examples/config_kraken.example.json index c55dea6ba..420047627 100644 --- a/config_examples/config_kraken.example.json +++ b/config_examples/config_kraken.example.json @@ -64,20 +64,6 @@ "pairlists": [ {"method": "StaticPairList"} ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, "telegram": { "enabled": false, "token": "your_telegram_token", diff --git a/docker-compose.yml b/docker-compose.yml index 445fbaea0..3b6f45bfc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,15 @@ services: # image: freqtradeorg/freqtrade:develop # Use plotting image # image: freqtradeorg/freqtrade:develop_plot + # # Enable GPU Image and GPU Resources (only relevant for freqAI) + # # Make sure to uncomment the whole deploy section + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] # Build step - only needed when additional dependencies are needed # build: # context: . @@ -16,7 +25,7 @@ services: - "./user_data:/freqtrade/user_data" # Expose api on port 8080 (localhost only) # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation - # before enabling this. + # for more information. ports: - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` diff --git a/docker/Dockerfile.armhf b/docker/Dockerfile.armhf index 7b663ae6c..4972e7109 100644 --- a/docker/Dockerfile.armhf +++ b/docker/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM python:3.9.12-slim-bullseye as base +FROM python:3.9.16-slim-bullseye as base # Setup env ENV LANG C.UTF-8 diff --git a/docker/docker-compose-freqai.yml b/docker/docker-compose-freqai.yml new file mode 100644 index 000000000..6edf41238 --- /dev/null +++ b/docker/docker-compose-freqai.yml @@ -0,0 +1,36 @@ +--- +version: '3' +services: + freqtrade: + image: freqtradeorg/freqtrade:stable_freqaitorch + # # Enable GPU Image and GPU Resources + # # Make sure to uncomment the whole deploy section + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + + # Build step - only needed when additional dependencies are needed + # build: + # context: . + # dockerfile: "./docker/Dockerfile.custom" + restart: unless-stopped + container_name: freqtrade + volumes: + - "./user_data:/freqtrade/user_data" + # Expose api on port 8080 (localhost only) + # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation + # for more information. + ports: + - "127.0.0.1:8080:8080" + # Default command used when running `docker compose up` + command: > + trade + --logfile /freqtrade/user_data/logs/freqtrade.log + --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite + --config /freqtrade/user_data/config.json + --freqai-model XGBoostClassifier + --strategy SampleStrategy diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index ae3eb2e4e..b587c4157 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -29,20 +29,22 @@ If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` `user_data/backtest_results` folder. To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command -with `--analysis-groups` option provided with space-separated arguments (default `0 1 2`): +with `--analysis-groups` option provided with space-separated arguments: ``` bash -freqtrade backtesting-analysis -c --analysis-groups 0 1 2 3 4 +freqtrade backtesting-analysis -c --analysis-groups 0 1 2 3 4 5 ``` This command will read from the last backtesting results. The `--analysis-groups` option is used to specify the various tabular outputs showing the profit fo each group or trade, ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4): +* 0: overall winrate and profit summary by enter_tag * 1: profit summaries grouped by enter_tag * 2: profit summaries grouped by enter_tag and exit_tag * 3: profit summaries grouped by pair and enter_tag * 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large) +* 5: profit summaries grouped by exit_tag More options are available by running with the `-h` option. @@ -114,3 +116,38 @@ For example, if your backtest timerange was `20220101-20221231` but you only wan ```bash freqtrade backtesting-analysis -c --timerange 20220101-20220201 ``` + +### Printing out rejected signals + +Use the `--rejected-signals` option to print out rejected signals. + +```bash +freqtrade backtesting-analysis -c --rejected-signals +``` + +### Writing tables to CSV + +Some of the tabular outputs can become large, so printing them out to the terminal is not preferable. +Use the `--analysis-to-csv` option to disable printing out of tables to standard out and write them to CSV files. + +```bash +freqtrade backtesting-analysis -c --analysis-to-csv +``` + +By default this will write one file per output table you specified in the `backtesting-analysis` command, e.g. + +```bash +freqtrade backtesting-analysis -c --analysis-to-csv --rejected-signals --analysis-groups 0 1 +``` + +This will write to `user_data/backtest_results`: + +* rejected_signals.csv +* group_0.csv +* group_1.csv + +To override where the files will be written, also specify the `--analysis-csv-path` option. + +```bash +freqtrade backtesting-analysis -c --analysis-to-csv --analysis-csv-path another/data/path/ +``` diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 0dace9985..ff0521f4f 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -75,7 +75,7 @@ This function needs to return a floating point number (`float`). Smaller numbers ## Overriding pre-defined spaces -To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows: +To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`, `max_open_trades_space`), define a nested class called Hyperopt and define the required spaces as follows: ```python from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal @@ -123,6 +123,12 @@ class MyAwesomeStrategy(IStrategy): Categorical([True, False], name='trailing_only_offset_is_reached'), ] + + # Define a custom max_open_trades space + def max_open_trades_space(self) -> List[Dimension]: + return [ + Integer(-1, 10, name='max_open_trades'), + ] ``` !!! Note diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 93a2025ed..54d489aa1 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -192,7 +192,7 @@ $RepeatedMsgReduction on ### Logging to journald -This needs the `systemd` python package installed as the dependency, which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows. +This needs the `cysystemd` python package installed as dependency (`pip install cysystemd`), which is not available on Windows. Hence, the whole journald logging functionality is not available for a bot running on Windows. To send Freqtrade log messages to `journald` system service use the `--logfile` command line option with the value in the following format: diff --git a/docs/assets/freqai_pytorch-diagram.png b/docs/assets/freqai_pytorch-diagram.png new file mode 100644 index 000000000..f48ebae25 Binary files /dev/null and b/docs/assets/freqai_pytorch-diagram.png differ diff --git a/docs/backtesting.md b/docs/backtesting.md index 0227df3f6..166c2b28b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -274,19 +274,20 @@ A backtesting result will look like that: | XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | | ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | -========================================================= EXIT REASON STATS ========================================================== -| Exit Reason | Exits | Wins | Draws | Losses | -|:-------------------|--------:|------:|-------:|--------:| -| trailing_stop_loss | 205 | 150 | 0 | 55 | -| stop_loss | 166 | 0 | 0 | 166 | -| exit_signal | 56 | 36 | 0 | 20 | -| force_exit | 2 | 0 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== | Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | |:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| | ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | | LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | | TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | +==================== EXIT REASON STATS ==================== +| Exit Reason | Exits | Wins | Draws | Losses | +|:-------------------|--------:|------:|-------:|--------:| +| trailing_stop_loss | 205 | 150 | 0 | 55 | +| stop_loss | 166 | 0 | 0 | 166 | +| exit_signal | 56 | 36 | 0 | 20 | +| force_exit | 2 | 0 | 0 | 2 | + ================== SUMMARY METRICS ================== | Metric | Value | |-----------------------------+---------------------| diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 3df926371..ef5e6900b 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -12,6 +12,9 @@ This page provides you some basic concepts on how Freqtrade works and operates. * **Indicators**: Technical indicators (SMA, EMA, RSI, ...). * **Limit order**: Limit orders which execute at the defined limit price or better. * **Market order**: Guaranteed to fill, may move price depending on the order size. +* **Current Profit**: Currently pending (unrealized) profit for this trade. This is mainly used throughout the bot and UI. +* **Realized Profit**: Already realized profit. Only relevant in combination with [partial exits](strategy-callbacks.md#adjust-trade-position) - which also explains the calculation logic for this. +* **Total Profit**: Combined realized and unrealized profit. The relative number (%) is calculated against the total investment in this trade. ## Fee handling @@ -57,10 +60,10 @@ This loop will be repeated again and again until the bot is stopped. * Load historic data for configured pairlist. * Calls `bot_start()` once. -* Calls `bot_loop_start()` once. * Calculate indicators (calls `populate_indicators()` once per pair). * Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair). * Loops per candle simulating entry and exit points. + * Calls `bot_loop_start()` strategy callback. * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks. * Calls `adjust_entry_price()` strategy callback for open entry orders. * Check for trade entry signals (`enter_long` / `enter_short` columns). @@ -75,3 +78,7 @@ This loop will be repeated again and again until the bot is stopped. !!! Note Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument. + +!!! Warning "Callback call frequency" + Backtesting will call each callback at max. once per candle (`--timeframe-detail` modifies this behavior to once per detailed candle). + Most callbacks will be called once per iteration in live (usually every ~5s) - which can cause backtesting mismatches. diff --git a/docs/configuration.md b/docs/configuration.md index 83b23425c..cf3872f1c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,7 @@ Per default, the bot loads the configuration from the `config.json` file, locate You can specify a different configuration file used by the bot with the `-c/--config` command-line option. -If you used the [Quick start](installation.md/#quick-start) method for installing +If you used the [Quick start](docker_quickstart.md#docker-quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file. @@ -134,11 +134,11 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Parameter | Description | |------------|-------------| -| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).
**Datatype:** Positive integer or -1. +| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Positive integer or -1. | `stake_currency` | **Required.** Crypto-currency used for trading.
**Datatype:** String | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. -| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. +| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
**Datatype:** Positive Float as ratio. @@ -155,25 +155,25 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) -| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates)
*Defaults to None.*
**Datatype:** Float +| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates)
*Defaults to `None`.*
**Datatype:** Float | `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md).
*Defaults to `"spot"`.*
**Datatype:** String | `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md).
**Datatype:** String | `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md).
*Defaults to `0.05`.*
**Datatype:** Float | | **Unfilled timeout** | `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String +| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `"minutes"`.*
**Datatype:** String | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | | **Pricing** -| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).
*Defaults to `"same"`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled). -| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean +| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `true`.*
**Datatype:** Boolean | `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer | `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
**Datatype:** Boolean | `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
**Datatype:** Float (as ratio) -| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).
*Defaults to `"same"`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled). -| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean +| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled).
*Defaults to `true`.*
**Datatype:** Boolean | `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)
*Defaults to `1`.*
**Datatype:** Positive Integer | `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price.
*Defaults to `0.02` 2%).*
**Datatype:** Positive float | | **TODO** @@ -199,10 +199,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer -| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean -| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`*
**Datatype:** Boolean +| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`*
**Datatype:** Boolean | `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".
*Defaults to `None`
**Datatype:** float -| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`*
**Datatype:** Boolean | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | | **Plugins** | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options. @@ -213,7 +213,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`.
**Datatype:** float -| `telegram.reload` | Allow "reload" buttons on telegram messages.
*Defaults to `True`.
**Datatype:** boolean +| `telegram.reload` | Allow "reload" buttons on telegram messages.
*Defaults to `true`.
**Datatype:** boolean | `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.
**Datatype:** dictionary | `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function.
**Datatype:** Boolean | | **Webhook** @@ -263,6 +263,7 @@ Values set in the configuration file always overwrite values set in the strategy * `minimal_roi` * `timeframe` * `stoploss` +* `max_open_trades` * `trailing_stop` * `trailing_stop_positive` * `trailing_stop_positive_offset` @@ -665,7 +666,7 @@ You should also make sure to read the [Exchanges](exchanges.md) section of the d ### Using proxy with Freqtrade To use a proxy with freqtrade, export your proxy settings using the variables `"HTTP_PROXY"` and `"HTTPS_PROXY"` set to the appropriate values. -This will have the proxy settings applied to everything (telegram, coingecko, ...) except exchange requests. +This will have the proxy settings applied to everything (telegram, coingecko, ...) **except** for exchange requests. ``` bash export HTTP_PROXY="http://addr:port" @@ -681,11 +682,12 @@ To use a proxy for exchange connections - you will have to define the proxies as { "exchange": { "ccxt_config": { - "aiohttp_proxy": "http://addr:port", - "proxies": { - "http": "http://addr:port", - "https": "http://addr:port" - }, + "aiohttp_proxy": "http://addr:port", + "proxies": { + "http": "http://addr:port", + "https": "http://addr:port" + }, + } } } ``` diff --git a/docs/deprecated.md b/docs/deprecated.md index 3b5b28b81..6719ce56d 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -74,3 +74,8 @@ Webhook terminology changed from "sell" to "exit", and from "buy" to "entry", re * `webhooksell`, `webhookexit` -> `exit` * `webhooksellfill`, `webhookexitfill` -> `exit_fill` * `webhooksellcancel`, `webhookexitcancel` -> `exit_cancel` + + +## Removal of `populate_any_indicators` + +version 2023.3 saw the removal of `populate_any_indicators` in favor of split methods for feature engineering and targets. Please read the [migration document](strategy_migration.md#freqai-strategy) for full details. diff --git a/docs/developer.md b/docs/developer.md index ea2e36ce1..2782f0117 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -24,7 +24,7 @@ This will spin up a local server (usually on port 8000) so you can see if everyt To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. -This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. +This will install all required tools for development, including `pytest`, `ruff`, `mypy`, and `coveralls`. Then install the git hook scripts by running `pre-commit install`, so your changes will be verified locally before committing. This avoids a lot of waiting for CI already, as some basic formatting checks are done locally on your machine. @@ -327,18 +327,18 @@ To check how the new exchange behaves, you can use the following snippet: ``` python import ccxt -from datetime import datetime +from datetime import datetime, timezone from freqtrade.data.converter import ohlcv_to_dataframe -ct = ccxt.binance() +ct = ccxt.binance() # Use the exchange you're testing timeframe = "1d" -pair = "XLM/BTC" # Make sure to use a pair that exists on that exchange! +pair = "BTC/USDT" # Make sure to use a pair that exists on that exchange! raw = ct.fetch_ohlcv(pair, timeframe=timeframe) # convert to dataframe df1 = ohlcv_to_dataframe(raw, timeframe, pair=pair, drop_incomplete=False) print(df1.tail(1)) -print(datetime.utcnow()) +print(datetime.now(timezone.utc)) ``` ``` output @@ -363,7 +363,7 @@ from pathlib import Path exchange = ccxt.binance({ 'apiKey': '', 'secret': '' - 'options': {'defaultType': 'future'} + 'options': {'defaultType': 'swap'} }) _ = exchange.load_markets() diff --git a/docs/exchanges.md b/docs/exchanges.md index 7070fc690..997d012e1 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -75,6 +75,25 @@ Binance has been split into 2, and users must use the correct ccxt exchange ID f * [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. * [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. +### Binance RSA keys + +Freqtrade supports binance RSA API keys. + +We recommend to use them as environment variable. + +``` bash +export FREQTRADE__EXCHANGE__SECRET="$(cat ./rsa_binance.private)" +``` + +They can however also be configured via configuration file. Since json doesn't support multi-line strings, you'll have to replace all newlines with `\n` to have a valid json file. + +``` json +// ... + "key": "", + "secret": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBABACAFQA<...>s8KX8=\n-----END PRIVATE KEY-----" +// ... +``` + ### Binance Futures Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders. @@ -224,8 +243,8 @@ OKX requires a passphrase for each api key, you will therefore need to add this OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode. !!! Warning "Futures" - OKX Futures has the concept of "position mode" - which can be Net or long/short (hedge mode). - Freqtrade supports both modes (we recommend to use net mode) - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades. + OKX Futures has the concept of "position mode" - which can be "Buy/Sell" or long/short (hedge mode). + Freqtrade supports both modes (we recommend to use Buy/Sell mode) - but changing the mode mid-trading is not supported and will lead to exceptions and failures to place trades. OKX also only provides MARK candles for the past ~3 months. Backtesting futures prior to that date will therefore lead to slight deviations, as funding-fees cannot be calculated correctly without this data. ## Gate.io @@ -236,6 +255,18 @@ OKX requires a passphrase for each api key, you will therefore need to add this Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0). The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value. +## Bybit + +Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. +Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures. +On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors. + +As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well. + +!!! Tip "Stoploss on Exchange" + Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. + On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. diff --git a/docs/faq.md b/docs/faq.md index bcceaf898..7b8cc2580 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,7 +2,7 @@ ## Supported Markets -Freqtrade supports spot trading only. +Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges. ### Can my bot open short positions? @@ -142,6 +142,13 @@ To fix this, redefine order types in the strategy to use "limit" instead of "mar The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy. +### I'm trying to start the bot live, but get an API permission error + +Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say. +Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console. +Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary. +Futures will usually have to be enabled specifically. + ### How do I search the bot logs for something? By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. @@ -248,8 +255,26 @@ The Edge module is mostly a result of brainstorming of [@mishaker](https://githu You can find further info on expectancy, win rate, risk management and position size in the following sources: - https://www.tradeciety.com/ultimate-math-guide-for-traders/ -- http://www.vantharp.com/tharp-concepts/expectancy.asp - https://samuraitradingacademy.com/trading-expectancy/ - https://www.learningmarkets.com/determining-expectancy-in-your-trading/ -- http://www.lonestocktrader.com/make-money-trading-positive-expectancy/ +- https://www.lonestocktrader.com/make-money-trading-positive-expectancy/ - https://www.babypips.com/trading/trade-expectancy-matter + +## Official channels + +Freqtrade is using exclusively the following official channels: + +* [Freqtrade discord server](https://discord.gg/p7nuUNVfP7) +* [Freqtrade documentation (https://freqtrade.io)](https://freqtrade.io) +* [Freqtrade github organization](https://github.com/freqtrade) + +Nobody affiliated with the freqtrade project will ask you about your exchange keys or anything else exposing your funds to exploitation. +Should you be asked to expose your exchange keys or send funds to some random wallet, then please don't follow these instructions. + +Failing to follow these guidelines will not be responsibility of freqtrade. + +## "Freqtrade token" + +Freqtrade does not have a Crypto token offering. + +Token offerings you find on the internet referring Freqtrade, FreqAI or freqUI must be considered to be a scam, trying to exploit freqtrade's popularity for their own, nefarious gains. diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 9d89800be..43c9fee75 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -9,7 +9,7 @@ FreqAI is configured through the typical [Freqtrade config file](configuration.m ```json "freqai": { "enabled": true, - "purge_old_models": true, + "purge_old_models": 2, "train_period_days": 30, "backtest_period_days": 7, "identifier" : "unique-id", @@ -52,7 +52,7 @@ The FreqAI strategy requires including the following lines of code in the standa return dataframe - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -77,7 +77,7 @@ The FreqAI strategy requires including the following lines of code in the standa return dataframe - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -101,7 +101,7 @@ The FreqAI strategy requires including the following lines of code in the standa dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -122,7 +122,7 @@ The FreqAI strategy requires including the following lines of code in the standa dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 return dataframe - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -139,6 +139,7 @@ The FreqAI strategy requires including the following lines of code in the standa / dataframe["close"] - 1 ) + return dataframe ``` Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`. @@ -165,10 +166,10 @@ Below are the values you can expect to include/use inside a typical strategy dat ## Setting the `startup_candle_count` -The `startup_candle_count` in the FreqAI strategy needs to be set up in the same way as in the standard Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., Ta-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`. +The `startup_candle_count` in the FreqAI strategy needs to be set up in the same way as in the standard Freqtrade strategy (see details [here](strategy-customization.md#strategy-startup-period)). This value is used by Freqtrade to ensure that a sufficient amount of data is provided when calling the `dataprovider`, to avoid any NaNs at the beginning of the first training. You can easily set this value by identifying the longest period (in candle units) which is passed to the indicator creation functions (e.g., TA-Lib functions). In the presented example, `startup_candle_count` is 20 since this is the maximum value in `indicators_periods_candles`. !!! Note - There are instances where the Ta-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean: + There are instances where the TA-Lib functions actually require more data than just the passed `period` or else the feature dataset gets populated with NaNs. Anecdotally, multiplying the `startup_candle_count` by 2 always leads to a fully NaN free training dataset. Hence, it is typically safest to multiply the expected `startup_candle_count` by 2. Look out for this log message to confirm that the data is clean: ``` 2022-08-31 15:14:04 - freqtrade.freqai.data_kitchen - INFO - dropped 0 training points due to NaNs in populated dataset 4319. @@ -205,7 +206,7 @@ All of the aforementioned model libraries implement gradient boosted decision tr * LightGBM: https://lightgbm.readthedocs.io/en/v3.3.2/# * XGBoost: https://xgboost.readthedocs.io/en/stable/# -There are also numerous online articles describing and comparing the algorithms. Some relatively light-weight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model. +There are also numerous online articles describing and comparing the algorithms. Some relatively lightweight examples would be [CatBoost vs. LightGBM vs. XGBoost — Which is the best algorithm?](https://towardsdatascience.com/catboost-vs-lightgbm-vs-xgboost-c80f40662924#:~:text=In%20CatBoost%2C%20symmetric%20trees%2C%20or,the%20same%20depth%20can%20differ.) and [XGBoost, LightGBM or CatBoost — which boosting algorithm should I use?](https://medium.com/riskified-technology/xgboost-lightgbm-or-catboost-which-boosting-algorithm-should-i-use-e7fda7bb36bc). Keep in mind that the performance of each model is highly dependent on the application and so any reported metrics might not be true for your particular use of the model. Apart from the models already available in FreqAI, it is also possible to customize and create your own prediction models using the `IFreqaiModel` class. You are encouraged to inherit `fit()`, `train()`, and `predict()` to customize various aspects of the training procedures. You can place custom FreqAI models in `user_data/freqaimodels` - and freqtrade will pick them up from there based on the provided `--freqaimodel` name - which has to correspond to the class name of your custom model. Make sure to use unique names to avoid overriding built-in models. @@ -236,3 +237,181 @@ If you want to predict multiple targets you must specify all labels in the same df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down') df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down']) ``` + +## PyTorch Module + +### Quick start + +The easiest way to quickly run a pytorch model is with the following command (for regression task): + +```bash +freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates +``` + +!!! Note "Installation/docker" + The PyTorch module requires large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]?". + Users who prefer docker should ensure they use the docker image appended with `_freqaitorch`. + We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. + This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available. + +### Structure + +#### Model + +You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks. + +```python + +class LogisticRegression(nn.Module): + def __init__(self, input_size: int): + super().__init__() + # Define your layers + self.linear = nn.Linear(input_size, 1) + self.activation = nn.Sigmoid() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # Define the forward pass + out = self.linear(x) + out = self.activation(out) + return out + +class MyCoolPyTorchClassifier(BasePyTorchClassifier): + """ + This is a custom IFreqaiModel showing how a user might setup their own + custom Neural Network architecture for their training. + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = LogisticRegression( + input_dim=n_features + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + data_convertor=self.data_convertor, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer + +``` + +#### Trainer + +The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop: +Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer. + +In addition, the trainer is responsible for the following: + - saving and loading the model + - converting the data from `pandas.DataFrame` to `torch.Tensor`. + +#### Integration with Freqai module + +Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. +From top to bottom: + +1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device` attribute used by children classes. Sets `model_type` attribute used by the parent class. +2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed. +3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects. + +![image](assets/freqai_pytorch-diagram.png) + +#### Full example + +Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. + +```python +class PyTorchMLPRegressor(BasePyTorchRegressor): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + init_model=init_model, + target_tensor_type=torch.float, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer +``` + +Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel`, where the former implements the `predict` method that is suitable for our regression task, and the latter implements the train method. + +??? Note "Setting Class Names for Classifiers" + When using classifiers, the user must declare the class names (or targets) by overriding the `IFreqaiModel.class_names` attribute. This is achieved by setting `self.freqai.class_names` in the FreqAI strategy inside the `set_freqai_targets` method. + + For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows: + ```python + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: + self.freqai.class_names = ["down", "up"] + dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > + dataframe["close"], 'up', 'down') + + return dataframe + ``` + To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py). + + +#### Improving performance with `torch.compile()` + +Torch provides a `torch.compile()` method that can be used to improve performance for specific GPU hardware. More details can be found [here](https://pytorch.org/tutorials/intermediate/torch_compile_tutorial.html). In brief, you simply wrap your `model` in `torch.compile()`: + + +```python + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + model = torch.compile(model) +``` + +Then proceed to use the model as normal. Keep in mind that doing this will remove eager execution, which means errors and tracebacks will not be informative. diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 6c8c5bb46..82b7569a5 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -6,9 +6,9 @@ Low level feature engineering is performed in the user strategy within a set of | Function | Description | |---------------|-------------| -| `feature_engineering__expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. -| `feature_engineering__expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`. -| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g. day of the week). +| `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. +| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`. +| `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week). | `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals. Meanwhile, high level feature engineering is handled within `"feature_parameters":{}` in the FreqAI config. Within this file, it is possible to decide large scale feature expansions on top of the `base_features` such as "including correlated pairs" or "including informative timeframes" or even "including recent candles." @@ -16,7 +16,7 @@ Meanwhile, high level feature engineering is handled within `"feature_parameters It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy: ```python - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -28,8 +28,13 @@ It is advisable to start from the template `feature_engineering_*` functions in All features must be prepended with `%` to be recognized by FreqAI internals. + Access metadata such as the current pair/timeframe/period with: + + `metadata["pair"]` `metadata["tf"]` `metadata["period"]` + :param df: strategy dataframe which will receive the features :param period: period of the indicator - usage example: + :param metadata: metadata of current pair dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) """ @@ -62,7 +67,7 @@ It is advisable to start from the template `feature_engineering_*` functions in return dataframe - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -75,9 +80,14 @@ It is advisable to start from the template `feature_engineering_*` functions in Features defined here will *not* be automatically duplicated on user defined `indicator_periods_candles` + Access metadata such as the current pair/timeframe with: + + `metadata["pair"]` `metadata["tf"]` + All features must be prepended with `%` to be recognized by FreqAI internals. :param df: strategy dataframe which will receive the features + :param metadata: metadata of current pair dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) """ @@ -86,7 +96,7 @@ It is advisable to start from the template `feature_engineering_*` functions in dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -98,22 +108,32 @@ It is advisable to start from the template `feature_engineering_*` functions in This function is a good place for any feature that should not be auto-expanded upon (e.g. day of the week). + Access metadata such as the current pair with: + + `metadata["pair"]` + All features must be prepended with `%` to be recognized by FreqAI internals. :param df: strategy dataframe which will receive the features + :param metadata: metadata of current pair usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 """ dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 return dataframe - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals. + Access metadata such as the current pair with: + + `metadata["pair"]` + :param df: strategy dataframe which will receive the targets + :param metadata: metadata of current pair usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] """ dataframe["&-s_close"] = ( @@ -161,6 +181,18 @@ You can ask for each of the defined features to be included also for informative In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles` $= 3 * 3 * 3 * 2 * 2 = 108$. +### Gain finer control over `feature_engineering_*` functions with `metadata` + +All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc. + +```python +def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame: + if metadata["tf"] == "1h": + dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) +``` + +This will block `ta.ROC()` from being added to any timeframes other than `"1h"`. + ### Returning additional info from training Important metrics can be returned to the strategy at the end of each model training by assigning them to `dk.data['extra_returns_per_train']['my_new_value'] = XYZ` inside the custom prediction model class. @@ -201,7 +233,7 @@ This will perform PCA on the features and reduce their dimensionality so that th ## Inlier metric -The `inlier_metric` is a metric aimed at quantifying how similar a the features of a data point are to the most recent historic data points. +The `inlier_metric` is a metric aimed at quantifying how similar the features of a data point are to the most recent historical data points. You define the lookback window by setting `inlier_metric_window` and FreqAI computes the distance between the present time point and each of the previous `inlier_metric_window` lookback points. A Weibull function is fit to each of the lookback distributions and its cumulative distribution function (CDF) is used to produce a quantile for each lookback point. The `inlier_metric` is then computed for each time point as the average of the corresponding lookback quantiles. The figure below explains the concept for an `inlier_metric_window` of 5. diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 046fa8008..cc92c2457 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -15,13 +15,13 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `identifier` | **Required.**
A unique ID for the current model. If models are saved to disk, the `identifier` allows for reloading specific pre-trained models/data.
**Datatype:** String. | `live_retrain_hours` | Frequency of retraining during dry/live runs.
**Datatype:** Float > 0.
Default: `0` (models retrain as often as possible). | `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old.
**Datatype:** Positive integer.
Default: `0` (models never expire). -| `purge_old_models` | Delete all unused models during live runs (not relevant to backtesting). If set to false (not default), dry/live runs will accumulate all unused models to disk. If
**Datatype:** Boolean.
Default: `True`. +| `purge_old_models` | Number of models to keep on disk (not relevant to backtesting). Default is 2, which means that dry/live runs will keep the latest 2 models on disk. Setting to 0 keeps all models. This parameter also accepts a boolean to maintain backwards compatibility.
**Datatype:** Integer.
Default: `2`. | `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`.
**Datatype:** Boolean.
Default: `False` (no models are saved). | `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)).
**Datatype:** Positive integer. -| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models.
**Datatype:** Boolean.
Default: `False`. -| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)).
**Datatype:** Boolean.
Default: `False`. +| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). Beware that this is currently a naive approach to incremental learning, and it has a high probability of overfitting/getting stuck in local minima while the market moves away from your model. We have the connections here primarily for experimental purposes and so that it is ready for more mature approaches to continual learning in chaotic systems like the crypto market.
**Datatype:** Boolean.
Default: `False`. | `write_metrics_to_disk` | Collect train timings, inference timings and cpu usage in json file.
**Datatype:** Boolean.
Default: `False` | `data_kitchen_thread_count` |
Designate the number of threads you want to use for data processing (outlier methods, normalization, etc.). This has no impact on the number of threads used for training. If user does not set it (default), FreqAI will use max number of threads - 2 (leaving 1 physical core available for Freqtrade bot and FreqUI)
**Datatype:** Positive integer. +| `activate_tensorboard` |
Indicate whether or not to activate tensorboard for the tensorboard enabled modules (currently Reinforcment Learning, XGBoost, Catboost, and PyTorch). Tensorboard needs Torch installed, which means you will need the torch/RL docker image or you need to answer "yes" to the install question about whether or not you wish to install Torch.
**Datatype:** Boolean.
Default: `True`. ### Feature parameters @@ -46,13 +46,15 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation).
**Datatype:** Integer.
Default: `0`. | `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset.
**Datatype:** Float.
Default: `30`. | `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it.
**Datatype:** Boolean.
Default: `False` (no reversal). +| `shuffle_after_split` | Split the data into train and test sets, and then shuffle both sets individually.
**Datatype:** Boolean.
Default: `False`. +| `buffer_train_data_candles` | Cut `buffer_train_data_candles` off the beginning and end of the training data *after* the indicators were populated. The main example use is when predicting maxima and minima, the argrelextrema function cannot know the maxima/minima at the edges of the timerange. To improve model accuracy, it is best to compute argrelextrema on the full timerange and then use this function to cut off the edges (buffer) by the kernel. In another case, if the targets are set to a shifted price movement, this buffer is unnecessary because the shifted candles at the end of the timerange will be NaN and FreqAI will automatically cut those off of the training dataset.
**Datatype:** Integer.
Default: `0`. ### Data split parameters | Parameter | Description | |------------|-------------| | | **Data split parameters within the `freqai.data_split_parameters` sub dictionary** -| `data_split_parameters` | Include any additional parameters available from Scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
**Datatype:** Dictionary. +| `data_split_parameters` | Include any additional parameters available from scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
**Datatype:** Dictionary. | `test_size` | The fraction of data that should be used for testing instead of training.
**Datatype:** Positive float < 1. | `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`.
**Datatype:** Boolean.
Defaut: `False`. @@ -83,12 +85,35 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `add_state_info` | Tell FreqAI to include state information in the feature set for training and inferencing. The current state variables include trade duration, current profit, trade position. This is only available in dry/live runs, and is automatically switched to false for backtesting.
**Datatype:** bool.
Default: `False`. | `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[, dict(vf=[], pi=[])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each. | `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting.
**Datatype:** bool.
Default: `False`. +| `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases)
**Datatype:** Boolean.
**Default:** `False` +| `progress_bar` | Display a progress bar with the current progress, elapsed time and estimated remaining time.
**Datatype:** Boolean.
Default: `False`. + +### PyTorch parameters + +#### general + +| Parameter | Description | +|------------|-------------| +| | **Model training parameters within the `freqai.model_training_parameters` sub dictionary** +| `learning_rate` | Learning rate to be passed to the optimizer.
**Datatype:** float.
Default: `3e-4`. +| `model_kwargs` | Parameters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. +| `trainer_kwargs` | Parameters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. + +#### trainer_kwargs + +| Parameter | Description | +|------------|-------------| +| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary** +| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs.
**Datatype:** int.
Default: `100`. +| `batch_size` | The size of the batches to use during training..
**Datatype:** int.
Default: `64`. +| `max_n_eval_batches` | The maximum number batches to use for evaluation..
**Datatype:** int, optional.
Default: `None`. + ### Additional parameters | Parameter | Description | |------------|-------------| | | **Extraneous parameters** -| `freqai.keras` | If the selected model makes use of Keras (typical for Tensorflow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards.
**Datatype:** Boolean.
Default: `False`. -| `freqai.conv_width` | The width of a convolutional neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction.
**Datatype:** Integer.
Default: `2`. +| `freqai.keras` | If the selected model makes use of Keras (typical for TensorFlow-based prediction models), this flag needs to be activated so that the model save/loading follows Keras standards.
**Datatype:** Boolean.
Default: `False`. +| `freqai.conv_width` | The width of a neural network input tensor. This replaces the need for shifting candles (`include_shifted_candles`) by feeding in historical data points as the second dimension of the tensor. Technically, this parameter can also be used for regressors, but it only adds computational overhead and does not change the model training/prediction.
**Datatype:** Integer.
Default: `2`. | `freqai.reduce_df_footprint` | Recast all numeric columns to float32/int32, with the objective of reducing ram/disk usage and decreasing train/inference timing. This parameter is set in the main level of the Freqtrade configuration file (not inside FreqAI).
**Datatype:** Boolean.
Default: `False`. diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index a09b4c5d0..1c95409ae 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -24,7 +24,7 @@ The framework is built on stable_baselines3 (torch) and OpenAI gym for the base ### Important considerations -As explained above, the agent is "trained" in an artificial trading "environment". In our case, that environment may seem quite similar to a real Freqtrade backtesting environment, but it is *NOT*. In fact, the RL training environment is much more simplified. It does not incorporate any of the complicated strategy logic, such as callbacks like `custom_exit`, `custom_stoploss`, leverage controls, etc. The RL environment is instead a very "raw" representation of the true market, where the agent has free-will to learn the policy (read: stoploss, take profit, etc.) which is enforced by the `calculate_reward()`. Thus, it is important to consider that the agent training environment is not identical to the real world. +As explained above, the agent is "trained" in an artificial trading "environment". In our case, that environment may seem quite similar to a real Freqtrade backtesting environment, but it is *NOT*. In fact, the RL training environment is much more simplified. It does not incorporate any of the complicated strategy logic, such as callbacks like `custom_exit`, `custom_stoploss`, leverage controls, etc. The RL environment is instead a very "raw" representation of the true market, where the agent has free will to learn the policy (read: stoploss, take profit, etc.) which is enforced by the `calculate_reward()`. Thus, it is important to consider that the agent training environment is not identical to the real world. ## Running Reinforcement Learning @@ -34,10 +34,10 @@ Setting up and running a Reinforcement Learning model is the same as running a R freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --config config.json ``` -where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesnt require them. However, FreqAI requires a default (neutral) value to be set in the action column: +where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column: ```python - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -52,17 +52,20 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from """ # For RL, there are no direct targets to set. This is filler (neutral) # until the agent sends an action. - df["&-action"] = 0 + dataframe["&-action"] = 0 + return dataframe ``` -Most of the function remains the same as for typical Regressors, however, the function above shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment: +Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment: ```python + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: # The following features are necessary for RL models - informative[f"%-{pair}raw_close"] = informative["close"] - informative[f"%-{pair}raw_open"] = informative["open"] - informative[f"%-{pair}raw_high"] = informative["high"] - informative[f"%-{pair}raw_low"] = informative["low"] + dataframe[f"%-raw_close"] = dataframe["close"] + dataframe[f"%-raw_open"] = dataframe["open"] + dataframe[f"%-raw_high"] = dataframe["high"] + dataframe[f"%-raw_low"] = dataframe["low"] + return dataframe ``` Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action. @@ -132,79 +135,104 @@ Parameter details can be found [here](freqai-parameter-table.md), but in general ## Creating a custom reward function -As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but users are encouraged to create their own custom reinforcement learning model class (see below) and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated: +!!! danger "Not for production" + Warning! + The reward function provided with the Freqtrade source code is a showcase of functionality designed to show/test as many possible environment control features as possible. It is also designed to run quickly on small computers. This is a benchmark, it is *not* for live production. Please beware that you will need to create your own custom_reward() function or use a template built by other users outside of the Freqtrade source code. + +As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but this is *not* designed for production. Users *must* create their own custom reinforcement learning model class or use a pre-built one from outside the Freqtrade source code and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated: + +!!! note "Hint" + The best reward functions are ones that are continuously differentiable, and well scaled. In other words, adding a single large negative penalty to a rare event is not a good idea, and the neural net will not be able to learn that function. Instead, it is better to add a small negative penalty to a common event. This will help the agent learn faster. Not only this, but you can help improve the continuity of your rewards/penalties by having them scale with severity according to some linear/exponential functions. In other words, you'd slowly scale the penalty as the duration of the trade increases. This is better than a single large penalty occuring at a single point in time. ```python - from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner - from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions +from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner +from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions - class MyCoolRLModel(ReinforcementLearner): +class MyCoolRLModel(ReinforcementLearner): + """ + User created RL prediction model. + + Save this file to `freqtrade/user_data/freqaimodels` + + then use it with: + + freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat + + Here the users can override any of the functions + available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this + is where the user overrides `MyRLEnv` (see below), to define custom + `calculate_reward()` function, or to override any other parts of the environment. + + This class also allows users to override any other part of the IFreqaiModel tree. + For example, the user can override `def fit()` or `def train()` or `def predict()` + to take fine-tuned control over these processes. + + Another common override may be `def data_cleaning_predict()` where the user can + take fine-tuned control over the data handling pipeline. + """ + class MyRLEnv(Base5ActionRLEnv): """ - User created RL prediction model. + User made custom environment. This class inherits from BaseEnvironment and gym.env. + Users can override any functions from those parent classes. Here is an example + of a user customized `calculate_reward()` function. - Save this file to `freqtrade/user_data/freqaimodels` - - then use it with: - - freqtrade trade --freqaimodel MyCoolRLModel --config config.json --strategy SomeCoolStrat - - Here the users can override any of the functions - available in the `IFreqaiModel` inheritance tree. Most importantly for RL, this - is where the user overrides `MyRLEnv` (see below), to define custom - `calculate_reward()` function, or to override any other parts of the environment. - - This class also allows users to override any other part of the IFreqaiModel tree. - For example, the user can override `def fit()` or `def train()` or `def predict()` - to take fine-tuned control over these processes. - - Another common override may be `def data_cleaning_predict()` where the user can - take fine-tuned control over the data handling pipeline. + Warning! + This is function is a showcase of functionality designed to show as many possible + environment control features as possible. It is also designed to run quickly + on small computers. This is a benchmark, it is *not* for live production. """ - class MyRLEnv(Base5ActionRLEnv): - """ - User made custom environment. This class inherits from BaseEnvironment and gym.env. - Users can override any functions from those parent classes. Here is an example - of a user customized `calculate_reward()` function. - """ - def calculate_reward(self, action: int) -> float: - # first, penalize if the action is not valid - if not self._is_valid(action): - return -2 - pnl = self.get_unrealized_profit() + def calculate_reward(self, action: int) -> float: + # first, penalize if the action is not valid + if not self._is_valid(action): + return -2 + pnl = self.get_unrealized_profit() - factor = 100 - # reward agent for entering trades - if action in (Actions.Long_enter.value, Actions.Short_enter.value) \ - and self._position == Positions.Neutral: - return 25 - # discourage agent from not entering trades - if action == Actions.Neutral.value and self._position == Positions.Neutral: - return -1 - max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300) - trade_duration = self._current_tick - self._last_trade_tick - if trade_duration <= max_trade_duration: - factor *= 1.5 - elif trade_duration > max_trade_duration: - factor *= 0.5 - # discourage sitting in position - if self._position in (Positions.Short, Positions.Long) and \ - action == Actions.Neutral.value: - return -1 * trade_duration / max_trade_duration - # close long - if action == Actions.Long_exit.value and self._position == Positions.Long: - if pnl > self.profit_aim * self.rr: - factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) - return float(pnl * factor) - # close short - if action == Actions.Short_exit.value and self._position == Positions.Short: - if pnl > self.profit_aim * self.rr: - factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) - return float(pnl * factor) - return 0. + factor = 100 + + pair = self.pair.replace(':', '') + + # you can use feature values from dataframe + # Assumes the shifted RSI indicator has been generated in the strategy. + rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_" + f"{self.config['timeframe']}"].iloc[self._current_tick] + + # reward agent for entering trades + if (action in (Actions.Long_enter.value, Actions.Short_enter.value) + and self._position == Positions.Neutral): + if rsi_now < 40: + factor = 40 / rsi_now + else: + factor = 1 + return 25 * factor + + # discourage agent from not entering trades + if action == Actions.Neutral.value and self._position == Positions.Neutral: + return -1 + max_trade_duration = self.rl_config.get('max_trade_duration_candles', 300) + trade_duration = self._current_tick - self._last_trade_tick + if trade_duration <= max_trade_duration: + factor *= 1.5 + elif trade_duration > max_trade_duration: + factor *= 0.5 + # discourage sitting in position + if self._position in (Positions.Short, Positions.Long) and \ + action == Actions.Neutral.value: + return -1 * trade_duration / max_trade_duration + # close long + if action == Actions.Long_exit.value and self._position == Positions.Long: + if pnl > self.profit_aim * self.rr: + factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) + return float(pnl * factor) + # close short + if action == Actions.Short_exit.value and self._position == Positions.Short: + if pnl > self.profit_aim * self.rr: + factor *= self.rl_config['model_reward_parameters'].get('win_reward_factor', 2) + return float(pnl * factor) + return 0. ``` -### Using Tensorboard +## Using Tensorboard Reinforcement Learning models benefit from tracking training metrics. FreqAI has integrated Tensorboard to allow users to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command: @@ -217,33 +245,30 @@ where `unique-id` is the `identifier` set in the `freqai` configuration file. Th ![tensorboard](assets/tensorboard.jpg) - -### Custom logging +## Custom logging FreqAI also provides a built in episodic summary logger called `self.tensorboard_log` for adding custom information to the Tensorboard log. By default, this function is already called once per step inside the environment to record the agent actions. All values accumulated for all steps in a single episode are reported at the conclusion of each episode, followed by a full reset of all metrics to 0 in preparation for the subsequent episode. - `self.tensorboard_log` can also be used anywhere inside the environment, for example, it can be added to the `calculate_reward` function to collect more detailed information about how often various parts of the reward were called: -```py - class MyRLEnv(Base5ActionRLEnv): - """ - User made custom environment. This class inherits from BaseEnvironment and gym.env. - Users can override any functions from those parent classes. Here is an example - of a user customized `calculate_reward()` function. - """ - def calculate_reward(self, action: int) -> float: - if not self._is_valid(action): - self.tensorboard_log("is_valid") - return -2 +```python + class MyRLEnv(Base5ActionRLEnv): + """ + User made custom environment. This class inherits from BaseEnvironment and gym.env. + Users can override any functions from those parent classes. Here is an example + of a user customized `calculate_reward()` function. + """ + def calculate_reward(self, action: int) -> float: + if not self._is_valid(action): + self.tensorboard_log("invalid") + return -2 ``` !!! Note - The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter. + The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)`. In this case the metric values are not incremented. - -### Choosing a base environment +## Choosing a base environment FreqAI provides three base environments, `Base3ActionRLEnvironment`, `Base4ActionEnvironment` and `Base5ActionEnvironment`. As the names imply, the environments are customized for agents that can select from 3, 4 or 5 actions. The `Base3ActionEnvironment` is the simplest, the agent can select from hold, long, or short. This environment can also be used for long-only bots (it automatically follows the `can_short` flag from the strategy), where long is the enter condition and short is the exit condition. Meanwhile, in the `Base4ActionEnvironment`, the agent can enter long, enter short, hold neutral, or exit position. Finally, in the `Base5ActionEnvironment`, the agent has the same actions as Base4, but instead of a single exit action, it separates exit long and exit short. The main changes stemming from the environment selection include: diff --git a/docs/freqai-running.md b/docs/freqai-running.md index a75e30e83..55f302d40 100644 --- a/docs/freqai-running.md +++ b/docs/freqai-running.md @@ -120,7 +120,7 @@ In the presented example config, the user will only allow predictions on models Model training parameters are unique to the selected machine learning library. FreqAI allows you to set any parameter for any library using the `model_training_parameters` dictionary in the config. The example config (found in `config_examples/config_freqai.example.json`) shows some of the example parameters associated with `Catboost` and `LightGBM`, but you can add any parameters available in those libraries or any other machine learning library you choose to implement. -Data split parameters are defined in `data_split_parameters` which can be any parameters associated with Scikit-learn's `train_test_split()` function. `train_test_split()` has a parameters called `shuffle` which allows to shuffle the data or keep it unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data. More details about these parameters can be found the [Scikit-learn website](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). +Data split parameters are defined in `data_split_parameters` which can be any parameters associated with scikit-learn's `train_test_split()` function. `train_test_split()` has a parameters called `shuffle` which allows to shuffle the data or keep it unshuffled. This is particularly useful to avoid biasing training with temporally auto-correlated data. More details about these parameters can be found the [scikit-learn website](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website). The FreqAI specific parameter `label_period_candles` defines the offset (number of candles into the future) used for the `labels`. In the presented [example config](freqai-configuration.md#setting-up-the-configuration-file), the user is asking for `labels` that are 24 candles in the future. @@ -128,6 +128,12 @@ The FreqAI specific parameter `label_period_candles` defines the offset (number You can choose to adopt a continual learning scheme by setting `"continual_learning": true` in the config. By enabling `continual_learning`, after training an initial model from scratch, subsequent trainings will start from the final model state of the preceding training. This gives the new model a "memory" of the previous state. By default, this is set to `False` which means that all new models are trained from scratch, without input from previous models. +???+ danger "Continual learning enforces a constant parameter space" + Since `continual_learning` means that the model parameter space *cannot* change between trainings, `principal_component_analysis` is automatically disabled when `continual_learning` is enabled. Hint: PCA changes the parameter space and the number of features, learn more about PCA [here](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis). + +???+ danger "Experimental functionality" + Beware that this is currently a naive approach to incremental learning, and it has a high probability of overfitting/getting stuck in local minima while the market moves away from your model. We have the mechanics available in FreqAI primarily for experimental purposes and so that it is ready for more mature approaches to continual learning in chaotic systems like the crypto market. + ## Hyperopt You can hyperopt using the same command as for [typical Freqtrade hyperopt](hyperopt.md): @@ -155,7 +161,14 @@ This specific hyperopt would help you understand the appropriate `DI_values` for ## Using Tensorboard -CatBoost models benefit from tracking training metrics via Tensorboard. You can take advantage of the FreqAI integration to track training and evaluation performance across all coins and across all retrainings. Tensorboard is activated via the following command: +!!! note "Availability" + FreqAI includes tensorboard for a variety of models, including XGBoost, all PyTorch models, Reinforcement Learning, and Catboost. If you would like to see Tensorboard integrated into another model type, please open an issue on the [Freqtrade GitHub](https://github.com/freqtrade/freqtrade/issues) + +!!! danger "Requirements" + Tensorboard logging requires the FreqAI torch installation/docker image. + + +The easiest way to use tensorboard is to ensure `freqai.activate_tensorboard` is set to `True` (default setting) in your configuration file, run FreqAI, then open a separate shell and run: ```bash cd freqtrade @@ -166,19 +179,6 @@ where `unique-id` is the `identifier` set in the `freqai` configuration file. Th ![tensorboard](assets/tensorboard.jpg) -## Setting up a follower -You can indicate to the bot that it should not train models, but instead should look for models trained by a leader with a specific `identifier` by defining: - -```json - "freqai": { - "enabled": true, - "follow_mode": true, - "identifier": "example", - "feature_parameters": { - // leader bots feature_parameters inserted here - }, - } -``` - -In this example, the user has a leader bot with the `"identifier": "example"`. The leader bot is already running or is launched simultaneously with the follower. The follower will load models created by the leader and inference them to obtain predictions instead of training its own models. The user will also need to duplicate the `feature_parameters` parameters from from the leaders freqai configuration file into the freqai section of the followers config. +!!! note "Deactivate for improved performance" + Tensorboard logging can slow down training and should be deactivated for production use. diff --git a/docs/freqai.md b/docs/freqai.md index d13d43f66..3c4f47212 100644 --- a/docs/freqai.md +++ b/docs/freqai.md @@ -4,7 +4,10 @@ ## Introduction -FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, the FreqAI aims to be a sand-box for easily deploying robust machine-learning libraries on real-time data ([details])(#freqai-position-in-open-source-machine-learning-landscape). +FreqAI is a software designed to automate a variety of tasks associated with training a predictive machine learning model to generate market forecasts given a set of input signals. In general, FreqAI aims to be a sandbox for easily deploying robust machine learning libraries on real-time data ([details](#freqai-position-in-open-source-machine-learning-landscape)). + +!!! Note + FreqAI is, and always will be, a not-for-profit, open-source project. FreqAI does *not* have a crypto token, FreqAI does *not* sell signals, and FreqAI does not have a domain besides the present [freqtrade documentation](https://www.freqtrade.io/en/latest/freqai/). Features include: @@ -19,7 +22,7 @@ Features include: * **Automatic data download** - Compute timeranges for data downloads and update historic data (in live deployments) * **Cleaning of incoming data** - Handle NaNs safely before training and model inferencing * **Dimensionality reduction** - Reduce the size of the training data via [Principal Component Analysis](freqai-feature-engineering.md#data-dimensionality-reduction-with-principal-component-analysis) -* **Deploying bot fleets** - Set one bot to train models while a fleet of [follower bots](freqai-running.md#setting-up-a-follower) inference the models and handle trades +* **Deploying bot fleets** - Set one bot to train models while a fleet of [consumers](producer-consumer.md) use signals. ## Quick start @@ -29,7 +32,10 @@ The easiest way to quickly test FreqAI is to run it in dry mode with the followi freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel LightGBMRegressor --strategy-path freqtrade/templates ``` -You will see the boot-up process of automatic data downloading, followed by simultaneous training and trading. +You will see the boot-up process of automatic data downloading, followed by simultaneous training and trading. + +!!! danger "Not for production" + The example strategy provided with the Freqtrade source code is designed for showcasing/testing a wide variety of FreqAI features. It is also designed to run on small computers so that it can be used as a benchmark between developers and users. It is *not* designed to be run in production. An example strategy, prediction model, and config to use as a starting points can be found in `freqtrade/templates/FreqaiExampleStrategy.py`, `freqtrade/freqai/prediction_models/LightGBMRegressor.py`, and @@ -66,15 +72,18 @@ pip install -r requirements-freqai.txt ``` !!! Note - Catboost will not be installed on arm devices (raspberry, Mac M1, ARM based VPS, ...), since it does not provide wheels for this platform. + Catboost will not be installed on low-powered arm devices (raspberry), since it does not provide wheels for this platform. ### Usage with docker -If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker-compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. +If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. + +!!! note "docker-compose-freqai.yml" + We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available. ### FreqAI position in open-source machine learning landscape -Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data-exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data-collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data. +Forecasting chaotic time-series based systems, such as equity/cryptocurrency markets, requires a broad set of tools geared toward testing a wide range of hypotheses. Fortunately, a recent maturation of robust machine learning libraries (e.g. `scikit-learn`) has opened up a wide range of research possibilities. Scientists from a diverse range of fields can now easily prototype their studies on an abundance of established machine learning algorithms. Similarly, these user-friendly libraries enable "citzen scientists" to use their basic Python skills for data exploration. However, leveraging these machine learning libraries on historical and live chaotic data sources can be logistically difficult and expensive. Additionally, robust data collection, storage, and handling presents a disparate challenge. [`FreqAI`](#freqai) aims to provide a generalized and extensible open-sourced framework geared toward live deployments of adaptive modeling for market forecasting. The `FreqAI` framework is effectively a sandbox for the rich world of open-source machine learning libraries. Inside the `FreqAI` sandbox, users find they can combine a wide variety of third-party libraries to test creative hypotheses on a free live 24/7 chaotic data source - cryptocurrency exchange data. ### Citing FreqAI diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e72b850ca..19bffd742 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -50,7 +50,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--eps] [--dmmp] [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [--timeframe-detail TIMEFRAME_DETAIL] [-e INT] - [--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]] + [--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] [--random-state INT] [--min-trades INT] [--hyperopt-loss NAME] [--disable-param-export] @@ -96,7 +96,7 @@ optional arguments: Specify detail timeframe for backtesting (`1m`, `5m`, `30m`, `1h`, `1d`). -e INT, --epochs INT Specify number of epochs (default: 100). - --spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...] + --spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...] Specify which parameters to hyperopt. Space-separated list. --print-all Print all results, not only the best ones. @@ -180,6 +180,7 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) * `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) +* `max_open_trades_space` - for custom max_open_trades optimization (if you need the ranges for the max_open_trades parameter in the optimization hyperspace that differ from default) !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss" You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. @@ -643,6 +644,7 @@ Legal values are: * `roi`: just optimize the minimal profit table for your strategy * `stoploss`: search for the best stoploss value * `trailing`: search for the best trailing stop values +* `trades`: search for the best max open trades values * `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these) * `default`: `all` except `trailing` and `protection` * space-separated list of any of the above values for example `--spaces roi stoploss` @@ -916,5 +918,5 @@ Once the optimized strategy has been implemented into your strategy, you should To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. Should results not match, please double-check to make sure you transferred all conditions correctly. -Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. -You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). +Pay special care to the stoploss, max_open_trades and trailing stoploss parameters, as these are often set in configuration files, which override changes to the strategy. +You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss`, `max_open_trades` or `trailing_stop`). diff --git a/docs/includes/protections.md b/docs/includes/protections.md index e0ad8189f..12af081c0 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -149,7 +149,7 @@ The below example assumes a timeframe of 1 hour: * Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled. * Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`). * Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`). -* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). +* Locks all pairs that had 2 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). * Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades. ``` python diff --git a/docs/index.md b/docs/index.md index 40b9e98ad..c24d1f36b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,6 +52,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Binance](https://www.binance.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [OKX](https://okx.com/) +- [X] [Bybit](https://bybit.com/) Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in. diff --git a/docs/installation.md b/docs/installation.md index 9dd14274a..a06968dba 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -46,7 +46,7 @@ These requirements apply to both [Script Installation](#script-installation) and * [pip](https://pip.pypa.io/en/stable/installing/) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) -* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions [below](#install-ta-lib)) +* [TA-Lib](https://ta-lib.github.io/ta-lib-python/) (install instructions [below](#install-ta-lib)) ### Install code @@ -204,7 +204,7 @@ sudo ./build_helpers/install_ta-lib.sh ##### TA-Lib manual installation -Official webpage: https://mrjbq7.github.io/ta-lib/install.html +[Official installation guide](https://ta-lib.github.io/ta-lib-python/install.html) ```bash wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz @@ -236,6 +236,7 @@ source .env/bin/activate ```bash python3 -m pip install --upgrade pip +python3 -m pip install -r requirements.txt python3 -m pip install -e . ``` @@ -284,10 +285,8 @@ cd freqtrade #### Freqtrade install: Conda Environment -Prepare conda-freqtrade environment, using file `environment.yml`, which exist in main freqtrade directory - ```bash -conda env create -n freqtrade-conda -f environment.yml +conda create --name freqtrade python=3.10 ``` !!! Note "Creating Conda Environment" @@ -296,12 +295,9 @@ conda env create -n freqtrade-conda -f environment.yml ```bash # choose your own packages conda env create -n [name of the environment] [python version] [packages] - - # point to file with packages - conda env create -n [name of the environment] -f [file] ``` -#### Enter/exit freqtrade-conda environment +#### Enter/exit freqtrade environment To check available environments, type @@ -313,7 +309,7 @@ Enter installed environment ```bash # enter conda environment -conda activate freqtrade-conda +conda activate freqtrade # exit conda environment - don't do it now conda deactivate @@ -323,6 +319,7 @@ Install last python dependencies with pip ```bash python3 -m pip install --upgrade pip +python3 -m pip install -r requirements.txt python3 -m pip install -e . ``` @@ -330,7 +327,7 @@ Patch conda libta-lib (Linux only) ```bash # Ensure that the environment is active! -conda activate freqtrade-conda +conda activate freqtrade cd build_helpers bash install_ta-lib.sh ${CONDA_PREFIX} nosudo @@ -349,8 +346,8 @@ conda env list # activate base environment conda activate -# activate freqtrade-conda environment -conda activate freqtrade-conda +# activate freqtrade environment +conda activate freqtrade #deactivate any conda environments conda deactivate diff --git a/docs/leverage.md b/docs/leverage.md index 0a265277e..deaa65896 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -67,8 +67,6 @@ You will also have to pick a "margin mode" (explanation below) - with freqtrade Freqtrade follows the [ccxt naming conventions for futures](https://docs.ccxt.com/en/latest/manual.html?#perpetual-swap-perpetual-future). A futures pair will therefore have the naming of `base/quote:settle` (e.g. `ETH/USDT:USDT`). -Binance is currently still an exception to this naming scheme, where pairs are named `ETH/USDT` also for futures markets, but will be aligned as soon as CCXT is ready. - ### Margin mode On top of `trading_mode` - you will also have to configure your `margin_mode`. diff --git a/docs/producer-consumer.md b/docs/producer-consumer.md index 88e34d0d6..0bd52ac93 100644 --- a/docs/producer-consumer.md +++ b/docs/producer-consumer.md @@ -42,14 +42,14 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect | `producers` | **Required.** List of producers
**Datatype:** Array. | `producers.name` | **Required.** Name of this producer. This name must be used in calls to `get_producer_pairs()` and `get_producer_df()` if more than one producer is used.
**Datatype:** string | `producers.host` | **Required.** The hostname or IP address from your producer.
**Datatype:** string -| `producers.port` | **Required.** The port matching the above host.
**Datatype:** string +| `producers.port` | **Required.** The port matching the above host.
*Defaults to `8080`.*
**Datatype:** Integer | `producers.secure` | **Optional.** Use ssl in websockets connection. Default False.
**Datatype:** string | `producers.ws_token` | **Required.** `ws_token` as configured on the producer.
**Datatype:** string | | **Optional settings** | `wait_timeout` | Timeout until we ping again if no message is received.
*Defaults to `300`.*
**Datatype:** Integer - in seconds. -| `wait_timeout` | Ping timeout
*Defaults to `10`.*
**Datatype:** Integer - in seconds. +| `ping_timeout` | Ping timeout
*Defaults to `10`.*
**Datatype:** Integer - in seconds. | `sleep_time` | Sleep time before retrying to connect.
*Defaults to `10`.*
**Datatype:** Integer - in seconds. -| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `10`.*
**Datatype:** Integer - in seconds. +| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `false`.*
**Datatype:** Boolean. | `message_size_limit` | Size limit per message
*Defaults to `8`.*
**Datatype:** Integer - Megabytes. Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index f615f5597..c5e478c78 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.0.3 +mkdocs==1.4.3 +mkdocs-material==9.1.14 mdx_truly_sane_lists==1.3 -pymdown-extensions==9.9 +pymdown-extensions==10.0.1 jinja2==3.1.2 diff --git a/docs/rest-api.md b/docs/rest-api.md index 62ad586dd..5b33bfa6f 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -9,9 +9,6 @@ This same command can also be used to update freqUI, should there be a new relea Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`). -!!! info "Alpha release" - FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose). - !!! Note "developers" Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. @@ -137,7 +134,9 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `reload_config` | Reloads the configuration file. | `trades` | List last trades. Limited to 500 trades per call. | `trade/` | Get specific trade. -| `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. +| `trade/` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. +| `trade//open-order` | DELETE - Cancel open order for this trade. +| `trade//reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. | `logs` | Shows last log messages. | `status` | Lists all open trades. @@ -163,7 +162,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `strategy ` | Get specific Strategy content. **Alpha** | `available_pairs` | List available backtest data. **Alpha** | `version` | Show version. -| `sysinfo` | Show informations about the system load. +| `sysinfo` | Show information about the system load. | `health` | Show bot health (last bot loop). !!! Warning "Alpha status" @@ -192,6 +191,11 @@ blacklist :param add: List of coins to add (example: "BNB/BTC") +cancel_open_order + Cancel open order for trade. + + :param trade_id: Cancels open orders for this trade. + count Return the amount of open trades. @@ -274,7 +278,6 @@ reload_config Reload configuration. show_config - Returns part of the configuration, relevant for trading operations. start @@ -320,6 +323,7 @@ version whitelist Show the current whitelist. + ``` ### Message WebSocket diff --git a/docs/stoploss.md b/docs/stoploss.md index 20e53d8f5..8fc73be21 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -23,10 +23,22 @@ These modes can be configured with these values: 'stoploss_on_exchange_limit_ratio': 0.99 ``` -!!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gateio (stop-limit), and Kucoin (stop-limit and stop-market) as of now. - Do not set too low/tight stoploss value if using stop loss on exchange! - If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. +Stoploss on exchange is only supported for the following exchanges, and not all exchanges support both stop-limit and stop-market. +The Order-type will be ignored if only one mode is available. + +| Exchange | stop-loss type | +|----------|-------------| +| Binance | limit | +| Binance Futures | market, limit | +| Huobi | limit | +| kraken | market, limit | +| Gate | limit | +| Okx | limit | +| Kucoin | stop-limit, stop-market| + +!!! Note "Tight stoploss" + Do not set too low/tight stoploss value when using stop loss on exchange! + If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work. ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio @@ -52,6 +64,18 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. +### stoploss_price_type + +!!! Warning "Only applies to futures" + `stoploss_price_type` only applies to futures markets (on exchanges where it's available). + Freqtrade will perform a validation of this setting on startup, failing to start if an invalid setting for your exchange has been selected. + Supported price types are gonna differs between each exchanges. Please check with your exchange on which price types it supports. + +Stoploss on exchange on futures markets can trigger on different price types. +The naming for these prices in exchange terminology often varies, but is usually something around "last" (or "contract price" ), "mark" and "index". + +Acceptable values for this setting are `"last"`, `"mark"` and `"index"` - which freqtrade will transfer automatically to the corresponding API type, and place the [stoploss on exchange](#stoploss_on_exchange-and-stoploss_on_exchange_limit_ratio) order correspondingly. + ### force_exit `force_exit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API. @@ -185,11 +209,6 @@ You can also keep a static stoploss until the offset is reached, and then trail If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. -``` python - trailing_stop_positive_offset = 0.011 - trailing_only_offset_is_reached = True -``` - Configuration (offset is buy-price + 3%): ``` python diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f55cda5e2..2749d1281 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -1,21 +1,21 @@ # Advanced Strategies This page explains some advanced concepts available for strategies. -If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first. +If you're just getting started, please familiarize yourself with the [Freqtrade basics](bot-basics.md) and methods described in [Strategy Customization](strategy-customization.md) first. -[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs. +The call sequence of the methods described here is covered under [bot execution logic](bot-basics.md#bot-execution-logic). Those docs are also helpful in deciding which method is most suitable for your customisation needs. !!! Note - All callback methods described below should only be implemented in a strategy if they are actually used. + Callback methods should *only* be implemented if a strategy uses them. !!! Tip - You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` + Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` ## Storing information Storing information can be accomplished by creating a new dictionary within the strategy class. -The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. +The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. ```python class AwesomeStrategy(IStrategy): @@ -80,7 +80,7 @@ class AwesomeStrategy(IStrategy): ## Enter Tag When your strategy has multiple buy signals, you can name the signal that triggered. -Then you can access you buy signal on `custom_exit` +Then you can access your buy signal on `custom_exit` ```python def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -227,8 +227,8 @@ for val in self.buy_ema_short.range: f'ema_short_{val}': ta.EMA(dataframe, timeperiod=val) })) -# Append columns to existing dataframe -merged_frame = pd.concat(frames, axis=1) +# Combine all dataframes, and reassign the original dataframe column +dataframe = pd.concat(frames, axis=1) ``` Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant. diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 19bd26a04..855f2353b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy): if self.config['runmode'].value in ('live', 'dry_run'): # Assign this to the class by using self.* # can then be used by populate_* methods - self.cust_remote_data = requests.get('https://some_remote_source.example.com') + self.custom_remote_data = requests.get('https://some_remote_source.example.com') ``` @@ -51,7 +51,8 @@ During hyperopt, this runs only once at startup. ## Bot loop start -A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently). +A simple callback which is called once at the start of every bot throttling iteration in dry/live mode (roughly every 5 +seconds, unless configured differently) or once per candle in backtest/hyperopt mode. This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. ``` python @@ -61,11 +62,12 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods - def bot_loop_start(self, **kwargs) -> None: + def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks (e.g. gather some remote resource for comparison) + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ if self.config['runmode'].value in ('live', 'dry_run'): @@ -316,11 +318,11 @@ class AwesomeStrategy(IStrategy): # evaluate highest to lowest, so that highest possible stop is used if current_profit > 0.40: - return stoploss_from_open(0.25, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.25, current_profit, is_short=trade.is_short, leverage=trade.leverage) elif current_profit > 0.25: - return stoploss_from_open(0.15, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.15, current_profit, is_short=trade.is_short, leverage=trade.leverage) elif current_profit > 0.20: - return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) # return maximum stoploss value, keeping current stoploss price unchanged return 1 @@ -350,7 +352,7 @@ class AwesomeStrategy(IStrategy): # Convert absolute price to percentage relative to current_rate if stoploss_price < current_rate: - return (stoploss_price / current_rate) - 1 + return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short) # return maximum stoploss value, keeping current stoploss price unchanged return 1 @@ -659,6 +661,7 @@ Position adjustments will always be applied in the direction of the trade, so a !!! Warning "Backtesting" During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected. + This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle. ``` python from freqtrade.persistence import Trade @@ -827,7 +830,7 @@ class AwesomeStrategy(IStrategy): """ # Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair. - if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10) > trade.open_date_utc: + if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10)) > trade.open_date_utc: # just cancel the order if it has been filled more than half of the amount if order.filled > order.remaining: return None diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 462f20402..8b6654c6c 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -881,7 +881,7 @@ All columns of the informative dataframe will be available on the returning data ### *stoploss_from_open()* -Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the open price instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired percentage above the open price. +Stoploss values returned from `custom_stoploss` must specify a percentage relative to `current_rate`, but sometimes you may want to specify a stoploss relative to the entry point instead. `stoploss_from_open()` is a helper function to calculate a stoploss value that can be returned from `custom_stoploss` which will be equivalent to the desired trade profit above the entry point. ??? Example "Returning a stoploss relative to the open price from the custom stoploss function" @@ -889,6 +889,8 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + This function will consider leverage - so at 10x leverage, the actual stoploss would be 0.7% above $100 (0.7% * 10x = 7%). + ``` python @@ -907,7 +909,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati # once the profit has risen above 10%, keep the stoploss at 7% above the open price if current_profit > 0.10: - return stoploss_from_open(0.07, current_profit, is_short=trade.is_short) + return stoploss_from_open(0.07, current_profit, is_short=trade.is_short, leverage=trade.leverage) return 1 @@ -954,12 +956,14 @@ In some situations it may be confusing to deal with stops relative to current ra ## Additional data (Wallets) -The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. +The strategy provides access to the `wallets` object. This contains the current balances on the exchange. -!!! Note - Wallets is not available during backtesting / hyperopt. +!!! Note "Backtesting / Hyperopt" + Wallets behaves differently depending on the function it's called. + Within `populate_*()` methods, it'll return the full wallet as configured. + Within [callbacks](strategy-callbacks.md), you'll get the wallet state corresponding to the actual simulated wallet at that point in the simulation process. -Please always check if `Wallets` is available to avoid failures during backtesting. +Please always check if `wallets` is available to avoid failures during backtesting. ``` python if self.wallets: @@ -1036,11 +1040,10 @@ from datetime import timedelta, datetime, timezone # Within populate indicators (or populate_buy): if self.config['runmode'].value in ('live', 'dry_run'): - # fetch closed trades for the last 2 days - trades = Trade.get_trades([Trade.pair == metadata['pair'], - Trade.open_date > datetime.utcnow() - timedelta(days=2), - Trade.is_open.is_(False), - ]).all() + # fetch closed trades for the last 2 days + trades = Trade.get_trades_proxy( + pair=metadata['pair'], is_open=False, + open_date=datetime.now(timezone.utc) - timedelta(days=2)) # Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy sumprofit = sum(trade.close_profit for trade in trades) if sumprofit < 0: diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index e3d2870e2..06dd33bc2 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -80,6 +80,7 @@ from freqtrade.resolvers import StrategyResolver from freqtrade.data.dataprovider import DataProvider strategy = StrategyResolver.load_strategy(config) strategy.dp = DataProvider(config, None, None) +strategy.ft_bot_start() # Generate buy/sell signals using strategy df = strategy.analyze_ticker(candles, {'pair': pair}) diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 22e3d2c22..5ef7a5a4c 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -578,7 +578,7 @@ def populate_any_indicators( Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed. ``` python linenums="1" - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe, period, **kwargs) -> DataFrame:: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -638,7 +638,7 @@ Features will now expand automatically. As such, the expansion loops, as well as Basic features. Make sure to remove the `{pair}` part from your features. ``` python linenums="1" - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame:: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -673,7 +673,7 @@ Basic features. Make sure to remove the `{pair}` part from your features. ### FreqAI - feature engineering standard ``` python linenums="1" - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -704,7 +704,7 @@ Basic features. Make sure to remove the `{pair}` part from your features. Targets now get their own, dedicated method. ``` python linenums="1" - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index db4a309d0..1b36c60ad 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -152,7 +152,7 @@ You can create your own keyboard in `config.json`: !!! Note "Supported Commands" Only the following commands are allowed. Command arguments are not supported! - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir` ## Telegram commands @@ -162,28 +162,38 @@ official commands. You can ask at any moment for help with `/help`. | Command | Description | |----------|-------------| +| **System commands** | `/start` | Starts the trader | `/stop` | Stops the trader | `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/reload_config` | Reloads the configuration file | `/show_config` | Shows part of the current configuration with relevant settings to operation | `/logs [limit]` | Show last log messages. +| `/help` | Show help message +| `/version` | Show version +| **Status** | | `/status` | Lists all open trades | `/status ` | Lists one or more specific trade. Separate multiple with a blank space. | `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/trades [limit]` | List all recently closed trades in a table format. -| `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). -| `/profit []` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) +| `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. If no direction is provided, the currently set direction will be displayed. +| **Modify Trade states** | | `/forceexit | /fx ` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`). | `/fx` | alias for `/forceexit` | `/forcelong [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`force_entry_enable` must be set to True) | `/forceshort [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`force_entry_enable` must be set to True) +| `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. +| `/reload_trade ` | Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange. +| `/cancel_open_order | /coo ` | Cancel an open order for a trade. +| **Metrics** | +| `/profit []` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/performance` | Show performance of each finished trade grouped by pair -| `/balance` | Show account balance per currency +| `/balance` | Show bot managed balance per currency +| `/balance full` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) | `/weekly ` | Shows profit or loss per week, over the last n weeks (n defaults to 8) | `/monthly ` | Shows profit or loss per month, over the last n months (n defaults to 6) @@ -193,8 +203,6 @@ official commands. You can ask at any moment for help with `/help`. | `/whitelist [sorted] [baseonly]` | Show the current whitelist. Optionally display in alphabetical order and/or with just the base currency of each pairing. | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/edge` | Show validated pairs by Edge if it is enabled. -| `/help` | Show help message -| `/version` | Show version ## Telegram commands in action @@ -236,7 +244,7 @@ Enter Tag is configurable via Strategy. > **Enter Tag:** Awesome Long Signal > **Open Rate:** `0.00007489` > **Current Rate:** `0.00007489` -> **Current Profit:** `12.95%` +> **Unrealized Profit:** `12.95%` > **Stoploss:** `0.00007389 (-0.02%)` ### /status table @@ -272,6 +280,7 @@ Return a summary of your profit/loss and performance. > ∙ `33.095 EUR` > > **Total Trade Count:** `138` +> **Bot started:** `2022-07-11 18:40:44` > **First Trade opened:** `3 days ago` > **Latest Trade opened:** `2 minutes ago` > **Avg. Duration:** `2:33:45` @@ -285,6 +294,7 @@ The relative profit of `15.2 Σ%` is be based on the starting capital - so in th Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. +Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date. ### /forceexit @@ -410,3 +420,27 @@ ARDR/ETH 0.366667 0.143059 -0.01 ### /version > **Version:** `0.14.3` + +### /marketdir + +If a market direction is provided the command updates the user managed variable that represents the current market direction. +This variable is not set to any valid market direction on bot startup and must be set by the user. The example below is for `/marketdir long`: + +``` +Successfully updated marketdirection from none to long. +``` + +If no market direction is provided the command outputs the currently set market directions. The example below is for `/marketdir`: + +``` +Currently set marketdirection: even +``` + +You can use the market direction in your strategy via `self.market_direction`. + +!!! Warning "Bot restarts" + Please note that the market direction is not persisted, and will be reset after a bot restart/reload. + +!!! Danger "Backtesting" + As this value/variable is intended to be changed manually in dry/live trading. + Strategies using `market_direction` will probably not produce reliable, reproducible results (changes to this variable will not be reflected for backtesting). Use at your own risk. diff --git a/docs/utils.md b/docs/utils.md index 87c7f6aa6..900856af4 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -723,6 +723,9 @@ usage: freqtrade backtesting-analysis [-h] [-v] [--logfile FILE] [-V] [--exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...]] [--indicator-list INDICATOR_LIST [INDICATOR_LIST ...]] [--timerange YYYYMMDD-[YYYYMMDD]] + [--rejected] + [--analysis-to-csv] + [--analysis-csv-path PATH] optional arguments: -h, --help show this help message and exit @@ -736,19 +739,27 @@ optional arguments: pair and enter_tag, 4: by pair, enter_ and exit_tag (this can get quite large) --enter-reason-list ENTER_REASON_LIST [ENTER_REASON_LIST ...] - Comma separated list of entry signals to analyse. - Default: all. e.g. 'entry_tag_a,entry_tag_b' + Space separated list of entry signals to analyse. + Default: all. e.g. 'entry_tag_a entry_tag_b' --exit-reason-list EXIT_REASON_LIST [EXIT_REASON_LIST ...] - Comma separated list of exit signals to analyse. + Space separated list of exit signals to analyse. Default: all. e.g. - 'exit_tag_a,roi,stop_loss,trailing_stop_loss' + 'exit_tag_a roi stop_loss trailing_stop_loss' --indicator-list INDICATOR_LIST [INDICATOR_LIST ...] - Comma separated list of indicators to analyse. e.g. - 'close,rsi,bb_lowerband,profit_abs' + Space separated list of indicators to analyse. e.g. + 'close rsi bb_lowerband profit_abs' --timerange YYYYMMDD-[YYYYMMDD] Timerange to filter trades for analysis, start inclusive, end exclusive. e.g. 20220101-20220201 + --rejected + Print out rejected trades table + --analysis-to-csv + Write out tables to individual CSVs, by default to + 'user_data/backtest_results' unless '--analysis-csv-path' is given. + --analysis-csv-path [PATH] + Optional path where individual CSVs will be written. If not used, + CSVs will be written to 'user_data/backtest_results'. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). @@ -955,3 +966,47 @@ Print trades with id 2 and 3 as json ``` bash freqtrade show-trades --db-url sqlite:///tradesv3.sqlite --trade-ids 2 3 --print-json ``` + +### Strategy-Updater + +Updates listed strategies or all strategies within the strategies folder to be v3 compliant. +If the command runs without --strategy-list then all strategies inside the strategies folder will be converted. +Your original strategy will remain available in the `user_data/strategies_orig_updater/` directory. + +!!! Warning "Conversion results" + Strategy updater will work on a "best effort" approach. Please do your due diligence and verify the results of the conversion. + We also recommend to run a python formatter (e.g. `black`) to format results in a sane manner. + +``` +usage: freqtrade strategy-updater [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] + [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] + +options: + -h, --help show this help message and exit + --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] + Provide a space-separated list of strategies to + backtest. Please note that timeframe needs to be set + either in config or via command line. When using this + together with `--export trades`, the strategy-name is + injected into the filename (so `backtest-data.json` + becomes `backtest-data-SampleStrategy.json` + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE, --log-file FILE + Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH, --data-dir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +``` diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 1b0d9d724..0327f21e5 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -24,9 +24,9 @@ git clone https://github.com/freqtrade/freqtrade.git Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). -As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.25-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version). +As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows. +These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade. -Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9 and 3.10) and for 64bit Windows. Other versions must be downloaded from the above link. ``` powershell @@ -45,8 +45,6 @@ freqtrade The above installation script assumes you're using powershell on a 64bit windows. Commands for the legacy CMD windows console may differ. -> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) - ### Error during installation on Windows ``` bash diff --git a/environment.yml b/environment.yml index 5298b2baa..e69de29bb 100644 --- a/environment.yml +++ b/environment.yml @@ -1,75 +0,0 @@ -name: freqtrade -channels: - - conda-forge -# - defaults -dependencies: -# 1/4 req main - - python>=3.8,<=3.10 - - numpy - - pandas - - pip - - - py-find-1st - - aiohttp - - SQLAlchemy - - python-telegram-bot - - arrow - - cachetools - - requests - - urllib3 - - jsonschema - - TA-Lib - - tabulate - - jinja2 - - blosc - - sdnotify - - fastapi - - uvicorn - - pyjwt - - aiofiles - - psutil - - colorama - - questionary - - prompt-toolkit - - schedule - - python-dateutil - - joblib - - pyarrow - - - # ============================ - # 2/4 req dev - - - coveralls - - flake8 - - mypy - - pytest - - pytest-asyncio - - pytest-cov - - pytest-mock - - isort - - nbconvert - - # ============================ - # 3/4 req hyperopt - - - scipy - - scikit-learn - - filelock - - scikit-optimize - - progressbar2 - # ============================ - # 4/4 req plot - - - plotly - - jupyter - - - pip: - - pycoingecko - # - py_find_1st - - tables - - pytest-random-order - - ccxt - - flake8-tidy-imports - - -e . - # - python-rapidjso diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 5430cd2d0..f8818c35c 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,19 +1,20 @@ """ Freqtrade bot """ -__version__ = '2023.1.dev' +__version__ = '2023.5.dev' if 'dev' in __version__: + from pathlib import Path try: import subprocess + freqtrade_basedir = Path(__file__).parent __version__ = __version__ + '-' + subprocess.check_output( ['git', 'log', '--format="%h"', '-n 1'], - stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"') + stderr=subprocess.DEVNULL, cwd=freqtrade_basedir).decode("utf-8").rstrip().strip('"') except Exception: # pragma: no cover # git not available, ignore try: # Try Fallback to freqtrade_commit file (created by CI while building docker image) - from pathlib import Path versionfile = Path('./freqtrade_commit') if versionfile.is_file(): __version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}" diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py old mode 100644 new mode 100755 diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 788657cc8..66a9c995b 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -22,5 +22,6 @@ from freqtrade.commands.optimize_commands import (start_backtesting, start_backt start_edge, start_hyperopt) from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit +from freqtrade.commands.strategy_utils_commands import start_strategy_update from freqtrade.commands.trade_commands import start_trading from freqtrade.commands.webserver_commands import start_webserver diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py old mode 100755 new mode 100644 index 20afa7ffd..e928ccad7 --- a/freqtrade/commands/analyze_commands.py +++ b/freqtrade/commands/analyze_commands.py @@ -40,8 +40,8 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s if (not Path(signals_file).exists()): raise OperationalException( - (f"Cannot find latest backtest signals file: {signals_file}." - "Run backtesting with `--export signals`.") + f"Cannot find latest backtest signals file: {signals_file}." + "Run backtesting with `--export signals`." ) return config diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b53a1022d..8287879c4 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -46,7 +46,7 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_coloriz ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] -ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"] +ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] @@ -106,15 +106,19 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop "disableparamexport", "backtest_breakdown"] ARGS_ANALYZE_ENTRIES_EXITS = ["exportfilename", "analysis_groups", "enter_reason_list", - "exit_reason_list", "indicator_list", "timerange"] + "exit_reason_list", "indicator_list", "timerange", + "analysis_rejected", "analysis_to_csv", "analysis_csv_path"] NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", "list-markets", "list-pairs", "list-strategies", "list-freqaimodels", "list-data", "hyperopt-list", "hyperopt-show", "backtest-filter", - "plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv"] + "plot-dataframe", "plot-profit", "show-trades", "trades-to-ohlcv", + "strategy-updater"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] +ARGS_STRATEGY_UTILS = ["strategy_list", "strategy_path", "recursive_strategy_search"] + class Arguments: """ @@ -198,8 +202,8 @@ class Arguments: start_list_freqAI_models, start_list_markets, start_list_strategies, start_list_timeframes, start_new_config, start_new_strategy, start_plot_dataframe, - start_plot_profit, start_show_trades, start_test_pairlist, - start_trading, start_webserver) + start_plot_profit, start_show_trades, start_strategy_update, + start_test_pairlist, start_trading, start_webserver) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added @@ -440,3 +444,11 @@ class Arguments: parents=[_common_parser]) webserver_cmd.set_defaults(func=start_webserver) self._build_args(optionlist=ARGS_WEBSERVER, parser=webserver_cmd) + + # Add strategy_updater subcommand + strategy_updater_cmd = subparsers.add_parser('strategy-updater', + help='updates outdated strategy' + 'files to the current version', + parents=[_common_parser]) + strategy_updater_cmd.set_defaults(func=start_strategy_update) + self._build_args(optionlist=ARGS_STRATEGY_UTILS, parser=strategy_updater_cmd) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index f95a08ba5..63bb5c211 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -108,7 +108,7 @@ def ask_user_config() -> Dict[str, Any]: "binance", "binanceus", "bittrex", - "gateio", + "gate", "huobi", "kraken", "kucoin", @@ -123,7 +123,7 @@ def ask_user_config() -> Dict[str, Any]: "message": "Do you want to trade Perpetual Swaps (perpetual futures)?", "default": False, "filter": lambda val: 'futures' if val else 'spot', - "when": lambda x: x["exchange_name"] in ['binance', 'gateio', 'okx'], + "when": lambda x: x["exchange_name"] in ['binance', 'gate', 'okx'], }, { "type": "autocomplete", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 91ac16365..f5e6d6926 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -251,7 +251,8 @@ AVAILABLE_CLI_OPTIONS = { "spaces": Arg( '--spaces', help='Specify which parameters to hyperopt. Space-separated list.', - choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'], + choices=['all', 'buy', 'sell', 'roi', 'stoploss', + 'trailing', 'protection', 'trades', 'default'], nargs='+', default='default', ), @@ -632,32 +633,48 @@ AVAILABLE_CLI_OPTIONS = { "1: by enter_tag, " "2: by enter_tag and exit_tag, " "3: by pair and enter_tag, " - "4: by pair, enter_ and exit_tag (this can get quite large)"), + "4: by pair, enter_ and exit_tag (this can get quite large), " + "5: by exit_tag"), nargs='+', - default=['0', '1', '2'], - choices=['0', '1', '2', '3', '4'], + default=[], + choices=['0', '1', '2', '3', '4', '5'], ), "enter_reason_list": Arg( "--enter-reason-list", - help=("Comma separated list of entry signals to analyse. Default: all. " - "e.g. 'entry_tag_a,entry_tag_b'"), + help=("Space separated list of entry signals to analyse. Default: all. " + "e.g. 'entry_tag_a entry_tag_b'"), nargs='+', default=['all'], ), "exit_reason_list": Arg( "--exit-reason-list", - help=("Comma separated list of exit signals to analyse. Default: all. " - "e.g. 'exit_tag_a,roi,stop_loss,trailing_stop_loss'"), + help=("Space separated list of exit signals to analyse. Default: all. " + "e.g. 'exit_tag_a roi stop_loss trailing_stop_loss'"), nargs='+', default=['all'], ), "indicator_list": Arg( "--indicator-list", - help=("Comma separated list of indicators to analyse. " - "e.g. 'close,rsi,bb_lowerband,profit_abs'"), + help=("Space separated list of indicators to analyse. " + "e.g. 'close rsi bb_lowerband profit_abs'"), nargs='+', default=[], ), + "analysis_rejected": Arg( + '--rejected-signals', + help='Analyse rejected signals', + action='store_true', + ), + "analysis_to_csv": Arg( + '--analysis-to-csv', + help='Save selected analysis tables to individual CSVs', + action='store_true', + ), + "analysis_csv_path": Arg( + '--analysis-csv-path', + help=("Specify a path to save the analysis CSVs " + "if --analysis-to-csv is enabled. Default: user_data/basktesting_results/"), + ), "freqaimodel": Arg( '--freqaimodel', help='Specify a custom freqaimodels.', diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 360387aa6..ed1571002 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from typing import Any, Dict, List from freqtrade.configuration import TimeRange, setup_utils_configuration -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import DATETIME_PRINT_FORMAT, Config from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) @@ -14,20 +14,30 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import market_is_active, timeframe_to_minutes from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist from freqtrade.resolvers import ExchangeResolver +from freqtrade.util.binance_mig import migrate_binance_futures_data logger = logging.getLogger(__name__) +def _data_download_sanity(config: Config) -> None: + if 'days' in config and 'timerange' in config: + raise OperationalException("--days and --timerange are mutually exclusive. " + "You can only specify one or the other.") + + if 'pairs' not in config: + raise OperationalException( + "Downloading data requires a list of pairs. " + "Please check the documentation on how to configure this.") + + def start_download_data(args: Dict[str, Any]) -> None: """ Download data (former download_backtest_data.py script) """ config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - if 'days' in config and 'timerange' in config: - raise OperationalException("--days and --timerange are mutually exclusive. " - "You can only specify one or the other.") + _data_download_sanity(config) timerange = TimeRange() if 'days' in config: time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d") @@ -39,15 +49,10 @@ def start_download_data(args: Dict[str, Any]) -> None: # Remove stake-currency to skip checks which are not relevant for datadownload config['stake_currency'] = '' - if 'pairs' not in config: - raise OperationalException( - "Downloading data requires a list of pairs. " - "Please check the documentation on how to configure this.") - pairs_not_available: List[str] = [] # Init exchange - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) + exchange = ExchangeResolver.load_exchange(config, validate=False) markets = [p for p, m in exchange.markets.items() if market_is_active(m) or config.get('include_inactive')] @@ -86,6 +91,7 @@ def start_download_data(args: Dict[str, Any]) -> None: "Please use `--dl-trades` instead for this exchange " "(will unfortunately take a long time)." ) + migrate_binance_futures_data(config) pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=expanded_pairs, timeframes=config['timeframes'], datadir=config['datadir'], timerange=timerange, @@ -119,7 +125,7 @@ def start_convert_trades(args: Dict[str, Any]) -> None: "Please check the documentation on how to configure this.") # Init exchange - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) + exchange = ExchangeResolver.load_exchange(config, validate=False) # Manual validations of relevant settings if not config['exchange'].get('skip_pair_validation', False): exchange.validate_pairs(config['pairs']) @@ -145,6 +151,7 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None: """ config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if ohlcv: + migrate_binance_futures_data(config) candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])] for candle_type in candle_types: convert_ohlcv_format(config, @@ -197,11 +204,14 @@ def start_list_data(args: Dict[str, Any]) -> None: pair, timeframe, candle_type, *dhc.ohlcv_data_min_max(pair, timeframe, candle_type) ) for pair, timeframe, candle_type in paircombs] + print(tabulate([ (pair, timeframe, candle_type, start.strftime(DATETIME_PRINT_FORMAT), end.strftime(DATETIME_PRINT_FORMAT)) - for pair, timeframe, candle_type, start, end in paircombs1 + for pair, timeframe, candle_type, start, end in sorted( + paircombs1, + key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])) ], headers=("Pair", "Timeframe", "Type", 'From', 'To'), tablefmt='psql', stralign='right')) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index c424016b1..d83605c6f 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -1,7 +1,7 @@ import logging from typing import Any, Dict -from sqlalchemy import func +from sqlalchemy import func, select from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.enums import RunMode @@ -20,7 +20,7 @@ def start_convert_db(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) init_db(config['db_url']) - session_target = Trade._session + session_target = Trade.session init_db(config['db_url_from']) logger.info("Starting db migration.") @@ -36,16 +36,16 @@ def start_convert_db(args: Dict[str, Any]) -> None: session_target.commit() - for pairlock in PairLock.query: + for pairlock in PairLock.get_all_locks(): pairlock_count += 1 make_transient(pairlock) session_target.add(pairlock) session_target.commit() # Update sequences - max_trade_id = session_target.query(func.max(Trade.id)).scalar() - max_order_id = session_target.query(func.max(Order.id)).scalar() - max_pairlock_id = session_target.query(func.max(PairLock.id)).scalar() + max_trade_id = session_target.scalar(select(func.max(Trade.id))) + max_order_id = session_target.scalar(select(func.max(Order.id))) + max_pairlock_id = session_target.scalar(select(func.max(PairLock.id))) set_sequence_ids(session_target.get_bind(), trade_id=max_trade_id, diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py old mode 100755 new mode 100644 diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 4e0623081..3358f8cc8 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -114,7 +114,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: config['timeframe'] = None # Init exchange - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) + exchange = ExchangeResolver.load_exchange(config, validate=False) if args['print_one_column']: print('\n'.join(exchange.timeframes)) @@ -133,7 +133,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Init exchange - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) + exchange = ExchangeResolver.load_exchange(config, validate=False) # By default only active pairs/markets are to be shown active_only = not args.get('list_pairs_all', False) diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index 9f7a5958e..a815cd5f3 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -18,7 +18,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: from freqtrade.plugins.pairlistmanager import PairListManager config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) + exchange = ExchangeResolver.load_exchange(config, validate=False) quote_currencies = args.get('quote_currencies') if not quote_currencies: diff --git a/freqtrade/commands/strategy_utils_commands.py b/freqtrade/commands/strategy_utils_commands.py new file mode 100644 index 000000000..e579ec475 --- /dev/null +++ b/freqtrade/commands/strategy_utils_commands.py @@ -0,0 +1,55 @@ +import logging +import sys +import time +from pathlib import Path +from typing import Any, Dict + +from freqtrade.configuration import setup_utils_configuration +from freqtrade.enums import RunMode +from freqtrade.resolvers import StrategyResolver +from freqtrade.strategy.strategyupdater import StrategyUpdater + + +logger = logging.getLogger(__name__) + + +def start_strategy_update(args: Dict[str, Any]) -> None: + """ + Start the strategy updating script + :param args: Cli args from Arguments() + :return: None + """ + + if sys.version_info == (3, 8): # pragma: no cover + sys.exit("Freqtrade strategy updater requires Python version >= 3.9") + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + strategy_objs = StrategyResolver.search_all_objects( + config, enum_failed=False, recursive=config.get('recursive_strategy_search', False)) + + filtered_strategy_objs = [] + if args['strategy_list']: + filtered_strategy_objs = [ + strategy_obj for strategy_obj in strategy_objs + if strategy_obj['name'] in args['strategy_list'] + ] + + else: + # Use all available entries. + filtered_strategy_objs = strategy_objs + + processed_locations = set() + for strategy_obj in filtered_strategy_objs: + if strategy_obj['location'] not in processed_locations: + processed_locations.add(strategy_obj['location']) + start_conversion(strategy_obj, config) + + +def start_conversion(strategy_obj, config): + print(f"Conversion of {Path(strategy_obj['location']).name} started.") + instance_strategy_updater = StrategyUpdater() + start = time.perf_counter() + instance_strategy_updater.start(config, strategy_obj) + elapsed = time.perf_counter() - start + print(f"Conversion of {Path(strategy_obj['location']).name} took {elapsed:.1f} seconds.") diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index 535844844..0707cc803 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -1,4 +1,5 @@ import logging +import signal from typing import Any, Dict @@ -12,15 +13,20 @@ def start_trading(args: Dict[str, Any]) -> int: # Import here to avoid loading worker module when it's not used from freqtrade.worker import Worker + def term_handler(signum, frame): + # Raise KeyboardInterrupt - so we can handle it in the same way as Ctrl-C + raise KeyboardInterrupt() + # Create and run worker worker = None try: + signal.signal(signal.SIGTERM, term_handler) worker = Worker(args) worker.run() except Exception as e: logger.error(str(e)) logger.exception("Fatal exception!") - except KeyboardInterrupt: + except (KeyboardInterrupt): logger.info('SIGINT received, aborting ...') finally: if worker: diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 606f081ef..0ee48cf91 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -27,10 +27,7 @@ def _extend_validator(validator_class): if 'default' in subschema: instance.setdefault(prop, subschema['default']) - for error in validate_properties( - validator, properties, instance, schema, - ): - yield error + yield from validate_properties(validator, properties, instance, schema) return validators.extend( validator_class, {'properties': set_defaults} diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 664610f33..8e9a7fd7c 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -28,7 +28,7 @@ class Configuration: Reuse this class for the bot, backtesting, hyperopt and every script that required configuration """ - def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None: + def __init__(self, args: Dict[str, Any], runmode: Optional[RunMode] = None) -> None: self.args = args self.config: Optional[Config] = None self.runmode = runmode @@ -465,6 +465,15 @@ class Configuration: self._args_to_config(config, argname='timerange', logstring='Filter trades by timerange: {}') + self._args_to_config(config, argname='analysis_rejected', + logstring='Analyse rejected signals: {}') + + self._args_to_config(config, argname='analysis_to_csv', + logstring='Store analysis tables to CSV: {}') + + self._args_to_config(config, argname='analysis_csv_path', + logstring='Path to store analysis CSVs: {}') + def _process_runmode(self, config: Config) -> None: self._args_to_config(config, argname='dry_run', diff --git a/freqtrade/configuration/environment_vars.py b/freqtrade/configuration/environment_vars.py index 473758fe1..d59d4bd23 100644 --- a/freqtrade/configuration/environment_vars.py +++ b/freqtrade/configuration/environment_vars.py @@ -32,7 +32,7 @@ def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str, :param prefix: Prefix to consider (usually FREQTRADE__) :return: Nested dict based on available and relevant variables. """ - no_convert = ['CHAT_ID'] + no_convert = ['CHAT_ID', 'PASSWORD'] relevant_vars: Dict[str, Any] = {} for env_var, val in sorted(env_dict.items()): diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 6d0321ba0..57424468d 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -6,7 +6,7 @@ import re import sys from copy import deepcopy from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import rapidjson @@ -58,7 +58,7 @@ def load_config_file(path: str) -> Dict[str, Any]: """ try: # Read config from stdin if requested in the options - with open(path) if path != '-' else sys.stdin as file: + with Path(path).open() if path != '-' else sys.stdin as file: config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) except FileNotFoundError: raise OperationalException( @@ -75,7 +75,8 @@ def load_config_file(path: str) -> Dict[str, Any]: return config -def load_from_files(files: List[str], base_path: Path = None, level: int = 0) -> Dict[str, Any]: +def load_from_files( + files: List[str], base_path: Optional[Path] = None, level: int = 0) -> Dict[str, Any]: """ Recursively load configuration files if specified. Sub-files are assumed to be relative to the initial config. diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index adc5e65df..cff35db7e 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -6,8 +6,6 @@ import re from datetime import datetime, timezone from typing import Optional -import arrow - from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.exceptions import OperationalException @@ -116,7 +114,7 @@ class TimeRange: :param text: value from --timerange :return: Start and End range period """ - if text is None: + if not text: return TimeRange(None, None, 0, 0) syntax = [(r'^-(\d{8})$', (None, 'date')), (r'^(\d{8})-$', ('date', None)), @@ -139,7 +137,8 @@ class TimeRange: if stype[0]: starts = rvals[index] if stype[0] == 'date' and len(starts) == 8: - start = arrow.get(starts, 'YYYYMMDD').int_timestamp + start = int(datetime.strptime(starts, '%Y%m%d').replace( + tzinfo=timezone.utc).timestamp()) elif len(starts) == 13: start = int(starts) // 1000 else: @@ -148,7 +147,8 @@ class TimeRange: if stype[1]: stops = rvals[index] if stype[1] == 'date' and len(stops) == 8: - stop = arrow.get(stops, 'YYYYMMDD').int_timestamp + stop = int(datetime.strptime(stops, '%Y%m%d').replace( + tzinfo=timezone.utc).timestamp()) elif len(stops) == 13: stop = int(stops) // 1000 else: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 397367216..3802ec3ad 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -5,7 +5,7 @@ bot constants """ from typing import Any, Dict, List, Literal, Tuple -from freqtrade.enums import CandleType, RPCMessageType +from freqtrade.enums import CandleType, PriceType, RPCMessageType DEFAULT_CONFIG = 'config.json' @@ -25,6 +25,7 @@ PRICING_SIDES = ['ask', 'bid', 'same', 'other'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] _ORDERTIF_POSSIBILITIES = ['GTC', 'FOK', 'IOC', 'PO'] ORDERTIF_POSSIBILITIES = _ORDERTIF_POSSIBILITIES + [t.lower() for t in _ORDERTIF_POSSIBILITIES] +STOPLOSS_PRICE_TYPES = [p for p in PriceType] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', @@ -35,9 +36,10 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', ' 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] -AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] -AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5'] -AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['feather', 'parquet'] +AVAILABLE_PROTECTIONS = ['CooldownPeriod', + 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] +AVAILABLE_DATAHANDLERS_TRADES = ['json', 'jsongz', 'hdf5', 'feather'] +AVAILABLE_DATAHANDLERS = AVAILABLE_DATAHANDLERS_TRADES + ['parquet'] BACKTEST_BREAKDOWNS = ['day', 'week', 'month'] BACKTEST_CACHE_AGE = ['none', 'day', 'week', 'month'] BACKTEST_CACHE_DEFAULT = 'day' @@ -62,6 +64,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] FULL_DATAFRAME_THRESHOLD = 100 +CUSTOM_TAG_MAX_LENGTH = 255 ENV_VAR_PREFIX = 'FREQTRADE__' @@ -229,6 +232,7 @@ CONF_SCHEMA = { 'default': 'market'}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'}, + 'stoploss_price_type': {'type': 'string', 'enum': STOPLOSS_PRICE_TYPES}, 'stoploss_on_exchange_interval': {'type': 'number'}, 'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0, 'maximum': 1.0} @@ -544,7 +548,7 @@ CONF_SCHEMA = { "enabled": {"type": "boolean", "default": False}, "keras": {"type": "boolean", "default": False}, "write_metrics_to_disk": {"type": "boolean", "default": False}, - "purge_old_models": {"type": "boolean", "default": True}, + "purge_old_models": {"type": ["boolean", "number"], "default": 2}, "conv_width": {"type": "integer", "default": 1}, "train_period_days": {"type": "integer", "default": 0}, "backtest_period_days": {"type": "number", "default": 7}, @@ -566,7 +570,9 @@ CONF_SCHEMA = { "shuffle": {"type": "boolean", "default": False}, "nu": {"type": "number", "default": 0.1} }, - } + }, + "shuffle_after_split": {"type": "boolean", "default": False}, + "buffer_train_data_candles": {"type": "integer", "default": 0} }, "required": ["include_timeframes", "include_corr_pairlist", ] }, @@ -584,6 +590,7 @@ CONF_SCHEMA = { "rl_config": { "type": "object", "properties": { + "drop_ohlc_from_features": {"type": "boolean", "default": False}, "train_cycles": {"type": "integer"}, "max_trade_duration_candles": {"type": "integer"}, "add_state_info": {"type": "boolean", "default": False}, @@ -592,7 +599,8 @@ CONF_SCHEMA = { "model_type": {"type": "string", "default": "PPO"}, "policy_type": {"type": "string", "default": "MlpPolicy"}, "net_arch": {"type": "array", "default": [128, 128]}, - "randomize_startinng_position": {"type": "boolean", "default": False}, + "randomize_starting_position": {"type": "boolean", "default": False}, + "progress_bar": {"type": "boolean", "default": True}, "model_reward_parameters": { "type": "object", "properties": { @@ -636,7 +644,6 @@ SCHEMA_TRADE_REQUIRED = [ SCHEMA_BACKTEST_REQUIRED = [ 'exchange', - 'max_open_trades', 'stake_currency', 'stake_amount', 'dry_run_wallet', @@ -646,6 +653,7 @@ SCHEMA_BACKTEST_REQUIRED = [ SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [ 'stoploss', 'minimal_roi', + 'max_open_trades' ] SCHEMA_MINIMAL_REQUIRED = [ @@ -679,5 +687,9 @@ EntryExit = Literal['entry', 'exit'] BuySell = Literal['buy', 'sell'] MakerTaker = Literal['maker', 'taker'] BidAsk = Literal['bid', 'ask'] +OBLiteral = Literal['asks', 'bids'] Config = Dict[str, Any] +# Exchange part of the configuration. +ExchangeConfig = Dict[str, Any] +IntOrInf = float diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3102683b2..c5905acde 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Union import numpy as np import pandas as pd -from freqtrade.constants import LAST_BT_RESULT_FN +from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf from freqtrade.exceptions import OperationalException from freqtrade.misc import json_load from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename @@ -90,7 +90,8 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str: return 'hyperopt_results.pickle' -def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = None) -> Path: +def get_latest_hyperopt_file( + directory: Union[Path, str], predef_filename: Optional[str] = None) -> Path: """ Get latest hyperopt export based on '.last_result.json'. :param directory: Directory to search for last result @@ -193,7 +194,7 @@ def get_backtest_resultlist(dirname: Path): def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], - min_backtest_date: datetime = None) -> Dict[str, Any]: + min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]: """ Find existing backtest stats that match specified run IDs and load them. :param dirname: pathlib.Path object, or string pointing to the file. @@ -245,14 +246,8 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame: """ Compatibility support for older backtest data. """ - df['open_date'] = pd.to_datetime(df['open_date'], - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - utc=True, - infer_datetime_format=True - ) + df['open_date'] = pd.to_datetime(df['open_date'], utc=True) + df['close_date'] = pd.to_datetime(df['close_date'], utc=True) # Compatibility support for pre short Columns if 'is_short' not in df.columns: df['is_short'] = False @@ -332,7 +327,7 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF def evaluate_result_multi(results: pd.DataFrame, timeframe: str, - max_open_trades: int) -> pd.DataFrame: + max_open_trades: IntOrInf) -> pd.DataFrame: """ Find overlapping trades by expanding each trade once per period it was open and then counting overlaps @@ -345,7 +340,7 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str, return df_final[df_final['open_trades'] > max_open_trades] -def trade_list_to_dataframe(trades: List[LocalTrade]) -> pd.DataFrame: +def trade_list_to_dataframe(trades: Union[List[Trade], List[LocalTrade]]) -> pd.DataFrame: """ Convert list of Trade objects to pandas Dataframe :param trades: List of trade objects @@ -372,7 +367,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF filters = [] if strategy: filters.append(Trade.strategy == strategy) - trades = trade_list_to_dataframe(Trade.get_trades(filters).all()) + trades = trade_list_to_dataframe(list(Trade.get_trades(filters).all())) return trades diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 7ce98de42..2d3855d87 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -34,7 +34,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *, cols = DEFAULT_DATAFRAME_COLUMNS df = DataFrame(ohlcv, columns=cols) - df['date'] = to_datetime(df['date'], unit='ms', utc=True, infer_datetime_format=True) + df['date'] = to_datetime(df['date'], unit='ms', utc=True) # Some exchanges return int values for Volume and even for OHLC. # Convert them since TA-LIB indicators used in the strategy assume floats diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index df4a4c898..d05ee5db7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -9,7 +9,7 @@ from collections import deque from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple -from pandas import DataFrame, to_timedelta +from pandas import DataFrame, Timedelta, Timestamp, to_timedelta from freqtrade.configuration import TimeRange from freqtrade.constants import (FULL_DATAFRAME_THRESHOLD, Config, ListPairsWithTimeframes, @@ -18,8 +18,10 @@ from freqtrade.data.history import load_pair_history from freqtrade.enums import CandleType, RPCMessageType, RunMode from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange, timeframe_to_seconds +from freqtrade.exchange.types import OrderBook from freqtrade.misc import append_candles_to_dataframe from freqtrade.rpc import RPCManager +from freqtrade.rpc.rpc_types import RPCAnalyzedDFMsg from freqtrade.util import PeriodicCache @@ -117,8 +119,7 @@ class DataProvider: :param new_candle: This is a new candle """ if self.__rpc: - self.__rpc.send_msg( - { + msg: RPCAnalyzedDFMsg = { 'type': RPCMessageType.ANALYZED_DF, 'data': { 'key': pair_key, @@ -126,7 +127,7 @@ class DataProvider: 'la': datetime.now(timezone.utc) } } - ) + self.__rpc.send_msg(msg) if new_candle: self.__rpc.send_msg({ 'type': RPCMessageType.NEW_CANDLE, @@ -206,9 +207,11 @@ class DataProvider: existing_df, _ = self.__producer_pairs_df[producer_name][pair_key] # CHECK FOR MISSING CANDLES - timeframe_delta = to_timedelta(timeframe) # Convert the timeframe to a timedelta for pandas - local_last = existing_df.iloc[-1]['date'] # We want the last date from our copy - incoming_first = dataframe.iloc[0]['date'] # We want the first date from the incoming + # Convert the timeframe to a timedelta for pandas + timeframe_delta: Timedelta = to_timedelta(timeframe) + local_last: Timestamp = existing_df.iloc[-1]['date'] # We want the last date from our copy + # We want the first date from the incoming + incoming_first: Timestamp = dataframe.iloc[0]['date'] # Remove existing candles that are newer than the incoming first candle existing_df1 = existing_df[existing_df['date'] < incoming_first] @@ -221,7 +224,7 @@ class DataProvider: # we missed some candles between our data and the incoming # so return False and candle_difference. if candle_difference > 1: - return (False, candle_difference) + return (False, int(candle_difference)) if existing_df1.empty: appended_df = dataframe else: @@ -281,7 +284,7 @@ class DataProvider: def historic_ohlcv( self, pair: str, - timeframe: str = None, + timeframe: Optional[str] = None, candle_type: str = '' ) -> DataFrame: """ @@ -333,7 +336,7 @@ class DataProvider: def get_pair_dataframe( self, pair: str, - timeframe: str = None, + timeframe: Optional[str] = None, candle_type: str = '' ) -> DataFrame: """ @@ -415,16 +418,14 @@ class DataProvider: def refresh(self, pairlist: ListPairsWithTimeframes, - helping_pairs: ListPairsWithTimeframes = None) -> None: + helping_pairs: Optional[ListPairsWithTimeframes] = None) -> None: """ Refresh data, called with each cycle """ if self._exchange is None: raise OperationalException(NO_EXCHANGE_EXCEPTION) - if helping_pairs: - self._exchange.refresh_latest_ohlcv(pairlist + helping_pairs) - else: - self._exchange.refresh_latest_ohlcv(pairlist) + final_pairs = (pairlist + helping_pairs) if helping_pairs else pairlist + self._exchange.refresh_latest_ohlcv(final_pairs) @property def available_pairs(self) -> ListPairsWithTimeframes: @@ -439,7 +440,7 @@ class DataProvider: def ohlcv( self, pair: str, - timeframe: str = None, + timeframe: Optional[str] = None, copy: bool = True, candle_type: str = '' ) -> DataFrame: @@ -487,7 +488,7 @@ class DataProvider: except ExchangeError: return {} - def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: + def orderbook(self, pair: str, maximum: int) -> OrderBook: """ Fetch latest l2 orderbook data Warning: Does a network request - so use with common sense. diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py old mode 100755 new mode 100644 index baa1cca3a..db3a7d3a4 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +from typing import List import joblib import pandas as pd @@ -15,22 +16,31 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -def _load_signal_candles(backtest_dir: Path): +def _load_backtest_analysis_data(backtest_dir: Path, name: str): if backtest_dir.is_dir(): scpf = Path(backtest_dir, - Path(get_latest_backtest_filename(backtest_dir)).stem + "_signals.pkl" + Path(get_latest_backtest_filename(backtest_dir)).stem + "_" + name + ".pkl" ) else: - scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_signals.pkl") + scpf = Path(backtest_dir.parent / f"{backtest_dir.stem}_{name}.pkl") try: - scp = open(scpf, "rb") - signal_candles = joblib.load(scp) - logger.info(f"Loaded signal candles: {str(scpf)}") + with scpf.open("rb") as scp: + loaded_data = joblib.load(scp) + logger.info(f"Loaded {name} candles: {str(scpf)}") except Exception as e: - logger.error("Cannot load signal candles from pickled results: ", e) + logger.error(f"Cannot load {name} data from pickled results: ", e) + return None - return signal_candles + return loaded_data + + +def _load_rejected_signals(backtest_dir: Path): + return _load_backtest_analysis_data(backtest_dir, "rejected") + + +def _load_signal_candles(backtest_dir: Path): + return _load_backtest_analysis_data(backtest_dir, "signals") def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles): @@ -43,9 +53,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand for pair in pairlist: if pair in signal_candles[strategy_name]: analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators( - pair, - trades, - signal_candles[strategy_name][pair]) + pair, trades, signal_candles[strategy_name][pair]) except Exception as e: print(f"Cannot process entry/exit reasons for {strategy_name}: ", e) @@ -85,7 +93,7 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: return pd.DataFrame() -def _do_group_table_output(bigdf, glist): +def _do_group_table_output(bigdf, glist, csv_path: Path, to_csv=False, ): for g in glist: # 0: summary wins/losses grouped by enter tag if g == "0": @@ -116,7 +124,8 @@ def _do_group_table_output(bigdf, glist): sortcols = ['total_num_buys'] - _print_table(new, sortcols, show_index=True) + _print_table(new, sortcols, show_index=True, name="Group 0:", + to_csv=to_csv, csv_path=csv_path) else: agg_mask = {'profit_abs': ['count', 'sum', 'median', 'mean'], @@ -141,6 +150,12 @@ def _do_group_table_output(bigdf, glist): # 4: profit summaries grouped by pair, enter_ and exit_tag (this can get quite large) if g == "4": group_mask = ['pair', 'enter_reason', 'exit_reason'] + + # 5: profit summaries grouped by exit_tag + if g == "5": + group_mask = ['exit_reason'] + sortcols = ['exit_reason'] + if group_mask: new = bigdf.groupby(group_mask).agg(agg_mask).reset_index() new.columns = group_mask + agg_cols @@ -148,11 +163,24 @@ def _do_group_table_output(bigdf, glist): new['mean_profit_pct'] = new['mean_profit_pct'] * 100 new['total_profit_pct'] = new['total_profit_pct'] * 100 - _print_table(new, sortcols) + _print_table(new, sortcols, name=f"Group {g}:", + to_csv=to_csv, csv_path=csv_path) else: logger.warning("Invalid group mask specified.") +def _do_rejected_signals_output(rejected_signals_df: pd.DataFrame, + to_csv: bool = False, csv_path=None) -> None: + cols = ['pair', 'date', 'enter_tag'] + sortcols = ['date', 'pair', 'enter_tag'] + _print_table(rejected_signals_df[cols], + sortcols, + show_index=False, + name="Rejected Signals:", + to_csv=to_csv, + csv_path=csv_path) + + def _select_rows_within_dates(df, timerange=None, df_date_col: str = 'date'): if timerange: if timerange.starttype == 'date': @@ -186,38 +214,64 @@ def prepare_results(analysed_trades, stratname, return res_df -def print_results(res_df, analysis_groups, indicator_list): +def print_results(res_df: pd.DataFrame, analysis_groups: List[str], indicator_list: List[str], + csv_path: Path, rejected_signals=None, to_csv=False): if res_df.shape[0] > 0: if analysis_groups: - _do_group_table_output(res_df, analysis_groups) + _do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path) + if rejected_signals is not None: + if rejected_signals.empty: + print("There were no rejected signals.") + else: + _do_rejected_signals_output(rejected_signals, to_csv=to_csv, csv_path=csv_path) + + # NB this can be large for big dataframes! if "all" in indicator_list: - print(res_df) - elif indicator_list is not None: + _print_table(res_df, + show_index=False, + name="Indicators:", + to_csv=to_csv, + csv_path=csv_path) + elif indicator_list is not None and indicator_list: available_inds = [] for ind in indicator_list: if ind in res_df: available_inds.append(ind) ilist = ["pair", "enter_reason", "exit_reason"] + available_inds - _print_table(res_df[ilist], sortcols=['exit_reason'], show_index=False) + _print_table(res_df[ilist], + sortcols=['exit_reason'], + show_index=False, + name="Indicators:", + to_csv=to_csv, + csv_path=csv_path) else: print("\\No trades to show") -def _print_table(df, sortcols=None, show_index=False): +def _print_table(df: pd.DataFrame, sortcols=None, *, show_index=False, name=None, + to_csv=False, csv_path: Path): if (sortcols is not None): data = df.sort_values(sortcols) else: data = df - print( - tabulate( - data, - headers='keys', - tablefmt='psql', - showindex=show_index + if to_csv: + safe_name = Path(csv_path, name.lower().replace(" ", "_").replace(":", "") + ".csv") + data.to_csv(safe_name) + print(f"Saved {name} to {safe_name}") + else: + if name is not None: + print(name) + + print( + tabulate( + data, + headers='keys', + tablefmt='psql', + showindex=show_index + ) ) - ) def process_entry_exit_reasons(config: Config): @@ -226,6 +280,11 @@ def process_entry_exit_reasons(config: Config): enter_reason_list = config.get('enter_reason_list', ["all"]) exit_reason_list = config.get('exit_reason_list', ["all"]) indicator_list = config.get('indicator_list', []) + do_rejected = config.get('analysis_rejected', False) + to_csv = config.get('analysis_to_csv', False) + csv_path = Path(config.get('analysis_csv_path', config['exportfilename'])) + if to_csv and not csv_path.is_dir(): + raise OperationalException(f"Specified directory {csv_path} does not exist.") timerange = TimeRange.parse_timerange(None if config.get( 'timerange') is None else str(config.get('timerange'))) @@ -235,8 +294,16 @@ def process_entry_exit_reasons(config: Config): for strategy_name, results in backtest_stats['strategy'].items(): trades = load_backtest_data(config['exportfilename'], strategy_name) - if not trades.empty: + if trades is not None and not trades.empty: signal_candles = _load_signal_candles(config['exportfilename']) + + rej_df = None + if do_rejected: + rejected_signals_dict = _load_rejected_signals(config['exportfilename']) + rej_df = prepare_results(rejected_signals_dict, strategy_name, + enter_reason_list, exit_reason_list, + timerange=timerange) + analysed_trades_dict = _process_candles_and_indicators( config['exchange']['pair_whitelist'], strategy_name, trades, signal_candles) @@ -247,7 +314,10 @@ def process_entry_exit_reasons(config: Config): print_results(res_df, analysis_groups, - indicator_list) + indicator_list, + rejected_signals=rej_df, + to_csv=to_csv, + csv_path=csv_path) except ValueError as e: raise OperationalException(e) from e diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/featherdatahandler.py index 22a6805e7..28a12fb29 100644 --- a/freqtrade/data/history/featherdatahandler.py +++ b/freqtrade/data/history/featherdatahandler.py @@ -4,7 +4,7 @@ from typing import Optional from pandas import DataFrame, read_feather, to_datetime from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, TradeList +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList from freqtrade.enums import CandleType from .idatahandler import IDataHandler @@ -63,10 +63,7 @@ class FeatherDataHandler(IDataHandler): pairdata.columns = self._columns pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( @@ -92,12 +89,11 @@ class FeatherDataHandler(IDataHandler): :param data: List of Lists containing trade data, column sequence as in DEFAULT_TRADES_COLUMNS """ - # filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair) + self.create_dir_if_needed(filename) - raise NotImplementedError() - # array = pa.array(data) - # array - # feather.write_feather(data, filename) + tradesdata = DataFrame(data, columns=DEFAULT_TRADES_COLUMNS) + tradesdata.to_feather(filename, compression_level=9, compression='lz4') def trades_append(self, pair: str, data: TradeList): """ @@ -116,14 +112,13 @@ class FeatherDataHandler(IDataHandler): :param timerange: Timerange to load trades for - currently not implemented :return: List of trades """ - raise NotImplementedError() - # filename = self._pair_trades_filename(self._datadir, pair) - # tradesdata = misc.file_load_json(filename) + filename = self._pair_trades_filename(self._datadir, pair) + if not filename.exists(): + return [] - # if not tradesdata: - # return [] + tradesdata = read_feather(filename) - # return tradesdata + return tradesdata.values.tolist() @classmethod def _get_file_extension(cls): diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 9a206baa4..dc3c7c1e6 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -1,10 +1,9 @@ import logging import operator -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from typing import Dict, List, Optional, Tuple -import arrow from pandas import DataFrame, concat from freqtrade.configuration import TimeRange @@ -28,8 +27,8 @@ def load_pair_history(pair: str, fill_up_missing: bool = True, drop_incomplete: bool = False, startup_candles: int = 0, - data_format: str = None, - data_handler: IDataHandler = None, + data_format: Optional[str] = None, + data_handler: Optional[IDataHandler] = None, candle_type: CandleType = CandleType.SPOT ) -> DataFrame: """ @@ -69,7 +68,7 @@ def load_data(datadir: Path, fail_without_data: bool = False, data_format: str = 'json', candle_type: CandleType = CandleType.SPOT, - user_futures_funding_rate: int = None, + user_futures_funding_rate: Optional[int] = None, ) -> Dict[str, DataFrame]: """ Load ohlcv history data for a list of pairs. @@ -116,7 +115,7 @@ def refresh_data(*, datadir: Path, timeframe: str, pairs: List[str], exchange: Exchange, - data_format: str = None, + data_format: Optional[str] = None, timerange: Optional[TimeRange] = None, candle_type: CandleType, ) -> None: @@ -189,7 +188,7 @@ def _download_pair_history(pair: str, *, timeframe: str = '5m', process: str = '', new_pairs_days: int = 30, - data_handler: IDataHandler = None, + data_handler: Optional[IDataHandler] = None, timerange: Optional[TimeRange] = None, candle_type: CandleType, erase: bool = False, @@ -236,8 +235,8 @@ def _download_pair_history(pair: str, *, new_data = exchange.get_historic_ohlcv(pair=pair, timeframe=timeframe, since_ms=since_ms if since_ms else - arrow.utcnow().shift( - days=-new_pairs_days).int_timestamp * 1000, + int((datetime.now() - timedelta(days=new_pairs_days) + ).timestamp()) * 1000, is_new_pair=data.empty, candle_type=candle_type, until_ms=until_ms if until_ms else None @@ -272,7 +271,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes datadir: Path, trading_mode: str, timerange: Optional[TimeRange] = None, new_pairs_days: int = 30, erase: bool = False, - data_format: str = None, + data_format: Optional[str] = None, prepend: bool = False, ) -> List[str]: """ @@ -349,7 +348,7 @@ def _download_trades_history(exchange: Exchange, trades = [] if not since: - since = arrow.utcnow().shift(days=-new_pairs_days).int_timestamp * 1000 + since = int((datetime.now() - timedelta(days=-new_pairs_days)).timestamp()) * 1000 from_id = trades[-1][1] if trades else None if trades and since < trades[-1][0]: diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 57441b4be..6637663ff 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -308,7 +308,7 @@ class IDataHandler(ABC): timerange=timerange_startup, candle_type=candle_type ) - if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True): + if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): return pairdf else: enddate = pairdf.iloc[-1]['date'] @@ -316,7 +316,7 @@ class IDataHandler(ABC): if timerange_startup: self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup) pairdf = trim_dataframe(pairdf, timerange_startup) - if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data): + if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data, True): return pairdf # incomplete candles should only be dropped if we didn't trim the end beforehand. @@ -374,6 +374,21 @@ class IDataHandler(ABC): logger.warning(f"{pair}, {candle_type}, {timeframe}, " f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}") + def rename_futures_data( + self, pair: str, new_pair: str, timeframe: str, candle_type: CandleType): + """ + Temporary method to migrate data from old naming to new naming (BTC/USDT -> BTC/USDT:USDT) + Only used for binance to support the binance futures naming unification. + """ + + file_old = self._pair_data_filename(self._datadir, pair, timeframe, candle_type) + file_new = self._pair_data_filename(self._datadir, new_pair, timeframe, candle_type) + # print(file_old, file_new) + if file_new.exists(): + logger.warning(f"{file_new} exists already, can't migrate {pair}.") + return + file_old.rename(file_new) + def get_datahandlerclass(datatype: str) -> Type[IDataHandler]: """ @@ -403,8 +418,8 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]: raise ValueError(f"No datahandler for datatype {datatype} available.") -def get_datahandler(datadir: Path, data_format: str = None, - data_handler: IDataHandler = None) -> IDataHandler: +def get_datahandler(datadir: Path, data_format: Optional[str] = None, + data_handler: Optional[IDataHandler] = None) -> IDataHandler: """ :param datadir: Folder to save data :param data_format: dataformat to use diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index f016c0ec1..ed7a33f8e 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -75,10 +75,7 @@ class JsonDataHandler(IDataHandler): return DataFrame(columns=self._columns) pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( diff --git a/freqtrade/data/history/parquetdatahandler.py b/freqtrade/data/history/parquetdatahandler.py index 57581861d..e6b2481d2 100644 --- a/freqtrade/data/history/parquetdatahandler.py +++ b/freqtrade/data/history/parquetdatahandler.py @@ -62,10 +62,7 @@ class ParquetDataHandler(IDataHandler): pairdata.columns = self._columns pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 4656b7c93..f2df0d3f2 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -3,9 +3,9 @@ import logging from collections import defaultdict from copy import deepcopy +from datetime import timedelta from typing import Any, Dict, List, NamedTuple -import arrow import numpy as np import utils_find_1st as utf1st from pandas import DataFrame @@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_seconds from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.strategy.interface import IStrategy +from freqtrade.util import dt_now logger = logging.getLogger(__name__) @@ -79,8 +80,8 @@ class Edge: self._stoploss_range_step ) - self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift( - days=-1 * self._since_number_of_days).format('YYYYMMDD')) + self._timerange: TimeRange = TimeRange.parse_timerange( + f"{(dt_now() - timedelta(days=self._since_number_of_days)).strftime('%Y%m%d')}-") if config.get('fee'): self.fee = config['fee'] else: @@ -97,7 +98,7 @@ class Edge: heartbeat = self.edge_config.get('process_throttle_secs') if (self._last_updated > 0) and ( - self._last_updated + heartbeat > arrow.utcnow().int_timestamp): + self._last_updated + heartbeat > int(dt_now().timestamp())): return False data: Dict[str, Any] = {} @@ -189,13 +190,13 @@ class Edge: # Fill missing, calculable columns, profit, duration , abs etc. trades_df = self._fill_calculable_fields(DataFrame(trades)) self._cached_pairs = self._process_expectancy(trades_df) - self._last_updated = arrow.utcnow().int_timestamp + self._last_updated = int(dt_now().timestamp()) return True def stake_amount(self, pair: str, free_capital: float, total_capital: float, capital_in_trade: float) -> float: - stoploss = self.stoploss(pair) + stoploss = self.get_stoploss(pair) available_capital = (total_capital + capital_in_trade) * self._capital_ratio allowed_capital_at_risk = available_capital * self._allowed_risk max_position_size = abs(allowed_capital_at_risk / stoploss) @@ -214,7 +215,7 @@ class Edge: ) return round(position_size, 15) - def stoploss(self, pair: str) -> float: + def get_stoploss(self, pair: str) -> float: if pair in self._cached_pairs: return self._cached_pairs[pair].stoploss else: diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index eb70a2894..69ef345e8 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -5,7 +5,9 @@ from freqtrade.enums.exitchecktuple import ExitCheckTuple from freqtrade.enums.exittype import ExitType from freqtrade.enums.hyperoptstate import HyperoptState from freqtrade.enums.marginmode import MarginMode +from freqtrade.enums.marketstatetype import MarketDirection from freqtrade.enums.ordertypevalue import OrderTypeValues +from freqtrade.enums.pricetype import PriceType from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType diff --git a/freqtrade/enums/candletype.py b/freqtrade/enums/candletype.py index 9d05ff6d7..dcb9f1448 100644 --- a/freqtrade/enums/candletype.py +++ b/freqtrade/enums/candletype.py @@ -13,6 +13,9 @@ class CandleType(str, Enum): FUNDING_RATE = "funding_rate" # BORROW_RATE = "borrow_rate" # * unimplemented + def __str__(self): + return f"{self.name.lower()}" + @staticmethod def from_string(value: str) -> 'CandleType': if not value: diff --git a/freqtrade/enums/exittype.py b/freqtrade/enums/exittype.py index b025230ba..c21b62667 100644 --- a/freqtrade/enums/exittype.py +++ b/freqtrade/enums/exittype.py @@ -15,6 +15,7 @@ class ExitType(Enum): EMERGENCY_EXIT = "emergency_exit" CUSTOM_EXIT = "custom_exit" PARTIAL_EXIT = "partial_exit" + SOLD_ON_EXCHANGE = "sold_on_exchange" NONE = "" def __str__(self): diff --git a/freqtrade/enums/marketstatetype.py b/freqtrade/enums/marketstatetype.py new file mode 100644 index 000000000..5cede32c2 --- /dev/null +++ b/freqtrade/enums/marketstatetype.py @@ -0,0 +1,15 @@ +from enum import Enum + + +class MarketDirection(Enum): + """ + Enum for various market directions. + """ + LONG = "long" + SHORT = "short" + EVEN = "even" + NONE = "none" + + def __str__(self): + # convert to string + return self.value diff --git a/freqtrade/enums/pricetype.py b/freqtrade/enums/pricetype.py new file mode 100644 index 000000000..bf0922b9f --- /dev/null +++ b/freqtrade/enums/pricetype.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class PriceType(str, Enum): + """Enum to distinguish possible trigger prices for stoplosses""" + LAST = "last" + MARK = "mark" + INDEX = "index" diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 2453d16d9..16d81b1d8 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -4,6 +4,7 @@ from enum import Enum class RPCMessageType(str, Enum): STATUS = 'status' WARNING = 'warning' + EXCEPTION = 'exception' STARTUP = 'startup' ENTRY = 'entry' @@ -37,5 +38,8 @@ class RPCRequestType(str, Enum): WHITELIST = 'whitelist' ANALYZED_DF = 'analyzed_df' + def __str__(self): + return self.value + NO_ECHO_MESSAGES = (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST, RPCMessageType.NEW_CANDLE) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index f706fd4dc..b5af1f1b2 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -10,6 +10,9 @@ class SignalType(Enum): ENTER_SHORT = "enter_short" EXIT_SHORT = "exit_short" + def __str__(self): + return f"{self.name.lower()}" + class SignalTagType(Enum): """ @@ -18,7 +21,13 @@ class SignalTagType(Enum): ENTER_TAG = "enter_tag" EXIT_TAG = "exit_tag" + def __str__(self): + return f"{self.name.lower()}" + class SignalDirection(str, Enum): LONG = 'long' SHORT = 'short' + + def __str__(self): + return f"{self.name.lower()}" diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 973ed499b..12fb0c55e 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,23 +1,24 @@ # flake8: noqa: F401 # isort: off -from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS +from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.exchange import Exchange # isort: on from freqtrade.exchange.binance import Binance from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bittrex import Bittrex +from freqtrade.exchange.bitvavo import Bitvavo from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.coinbasepro import Coinbasepro -from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts, - amount_to_precision, available_exchanges, - ccxt_exchanges, contracts_to_amount, - date_minus_candles, is_exchange_known_ccxt, - market_is_active, price_to_precision, - timeframe_to_minutes, timeframe_to_msecs, - timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds, validate_exchange, - validate_exchanges) -from freqtrade.exchange.gateio import Gateio +from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, + amount_to_contracts, amount_to_precision, + available_exchanges, ccxt_exchanges, + contracts_to_amount, date_minus_candles, + is_exchange_known_ccxt, market_is_active, + price_to_precision, timeframe_to_minutes, + timeframe_to_msecs, timeframe_to_next_date, + timeframe_to_prev_date, timeframe_to_seconds, + validate_exchange, validate_exchanges) +from freqtrade.exchange.gate import Gate from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.huobi import Huobi from freqtrade.exchange.kraken import Kraken diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 9942a4268..caca3eefb 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,13 +1,12 @@ """ Binance exchange subclass """ import logging -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import Dict, List, Optional, Tuple -import arrow import ccxt -from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.enums import CandleType, MarginMode, PriceType, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -23,16 +22,22 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "stop_loss_limit"}, - "order_time_in_force": ['GTC', 'FOK', 'IOC'], + "order_time_in_force": ["GTC", "FOK", "IOC", "PO"], "ohlcv_candle_limit": 1000, "trades_pagination": "id", "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], - "ccxt_futures_name": "future" } _ft_has_futures: Dict = { "stoploss_order_types": {"limit": "stop", "market": "stop_market"}, + "order_time_in_force": ["GTC", "FOK", "IOC"], "tickers_have_price": False, + "floor_leverage": True, + "stop_price_type_field": "workingType", + "stop_price_type_value_mapping": { + PriceType.LAST: "CONTRACT_PRICE", + PriceType.MARK: "MARK_PRICE", + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ @@ -78,33 +83,9 @@ class Binance(Exchange): raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' + ) from e - @retrier - def _set_leverage( - self, - leverage: float, - pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None - ): - """ - Set's the leverage before making a trade, in order to not - have the same leverage on every trade - """ - trading_mode = trading_mode or self.trading_mode - - if self._config['dry_run'] or trading_mode != TradingMode.FUTURES: - return - - try: - self._api.set_leverage(symbol=pair, leverage=round(leverage)) - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e @@ -123,8 +104,9 @@ class Binance(Exchange): if x and x[3] and x[3][0] and x[3][0][0] > since_ms: # Set starting date to first available candle. since_ms = x[3][0][0] - logger.info(f"Candle-data for {pair} available starting with " - f"{arrow.get(since_ms // 1000).isoformat()}.") + logger.info( + f"Candle-data for {pair} available starting with " + f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}.") return await super()._async_get_historic_ohlcv( pair=pair, @@ -150,6 +132,7 @@ class Binance(Exchange): is_short: bool, amount: float, stake_amount: float, + leverage: float, wallet_balance: float, # Or margin balance mm_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only @@ -159,11 +142,12 @@ class Binance(Exchange): MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - :param exchange_name: + :param pair: Pair to calculate liquidation price for :param open_rate: Entry price of position :param is_short: True if the trade is a short, false otherwise :param amount: Absolute value of position size incl. leverage (in base currency) :param stake_amount: Stake amount - Collateral in settle currency. + :param leverage: Leverage used for this position. :param trading_mode: SPOT, MARGIN, FUTURES, etc. :param margin_mode: Either ISOLATED or CROSS :param wallet_balance: Amount of margin_mode in the wallet being used to trade @@ -212,7 +196,7 @@ class Binance(Exchange): leverage_tiers_path = ( Path(__file__).parent / 'binance_leverage_tiers.json' ) - with open(leverage_tiers_path) as json_file: + with leverage_tiers_path.open() as json_file: return json_load(json_file) else: try: diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 09bf0a4dc..0f252f63e 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -1,87 +1,217 @@ { - "1000LUNC/BUSD": [ + "1000FLOKI/USDT:USDT": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", - "notionalCap": "25000", + "notionalCap": "5000", "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "300000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "800000", + "notionalFloor": "300000", + "maintMarginRatio": "0.1", + "cum": "15650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.125", + "cum": "35650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "910650.0" + } + } + ], + "1000LUNC/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, "maxLeverage": 10.0, "info": { "bracket": "2", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" + "cum": "5650.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "BUSD", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "11900.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "1", "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "386900.0" } } ], - "1000LUNC/USDT": [ + "1000LUNC/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -118,13 +248,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -133,262 +263,456 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "1000SHIB/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "1000SHIB/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", - "maintMarginRatio": "0.05", - "cum": "4500.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.1", - "cum": "17000.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "5", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "29500.0" + "cum": "45700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "6000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "154500.0" + "cum": "295700.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 30000000.0, + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "30000000", - "notionalFloor": "2000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "654500.0" + "cum": "1795700.0" } } ], - "1000XEC/USDT": [ + "1000PEPE/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "1000SHIB/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "949400.0" + } + } + ], + "1000SHIB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.0065", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.0075, + "maxLeverage": 40.0, + "info": { + "bracket": "2", + "initialLeverage": "40", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.0075", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "25000", "maintMarginRatio": "0.01", + "cum": "67.5" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "150000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "817.5" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 150000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "250000", + "notionalFloor": "150000", + "maintMarginRatio": "0.05", + "cum": "4567.5" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.1", + "cum": "17067.5" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "29567.5" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "154567.5" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.5", + "cum": "654567.5" + } + } + ], + "1000XEC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -405,7 +729,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -421,7 +745,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -437,7 +761,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -453,7 +777,7 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { @@ -469,11 +793,11 @@ "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "1INCH/USDT": [ + "1INCH/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -571,7 +895,7 @@ } } ], - "AAVE/USDT": [ + "AAVE/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -604,6 +928,120 @@ "cum": "75.0" } }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795700.0" + } + } + ], + "ACH/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, { "tier": 3.0, "currency": "USDT", @@ -617,7 +1055,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -633,7 +1071,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -649,7 +1087,7 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { @@ -665,11 +1103,11 @@ "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "ADA/BUSD": [ + "ADA/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", @@ -767,101 +1205,101 @@ } } ], - "ADA/USDT": [ + "ADA/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 50.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", - "notionalCap": "10000", + "initialLeverage": "75", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", "minNotional": 10000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 40.0, "info": { - "bracket": "2", + "bracket": "3", "initialLeverage": "40", "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "35.0" + "cum": "45.0" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "25", "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "535.0" + "cum": "545.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "10", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8035.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "5", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 1000000.0, "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "4", + "initialLeverage": "5", "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "108035.0" + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "58045.0" } }, { @@ -869,15 +1307,15 @@ "currency": "USDT", "minNotional": 5000000.0, "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "7", - "initialLeverage": "3", + "initialLeverage": "4", "notionalCap": "10000000", "notionalFloor": "5000000", - "maintMarginRatio": "0.15", - "cum": "233035.0" + "maintMarginRatio": "0.125", + "cum": "183045.0" } }, { @@ -885,48 +1323,162 @@ "currency": "USDT", "minNotional": 10000000.0, "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, "info": { "bracket": "8", - "initialLeverage": "2", + "initialLeverage": "3", "notionalCap": "20000000", "notionalFloor": "10000000", - "maintMarginRatio": "0.25", - "cum": "1233035.0" + "maintMarginRatio": "0.15", + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2433045.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "9", + "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233035.0" + "cum": "9933045.0" } } ], - "ALGO/USDT": [ + "AGIX/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 15.0, + "info": { + "bracket": "1", + "initialLeverage": "15", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "AGIX/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "1", "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -943,75 +1495,91 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820650.0" } } ], - "ALICE/USDT": [ + "ALGO/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -1109,282 +1677,20 @@ } } ], - "ALPHA/USDT": [ + "ALICE/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "AMB/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "ANC/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "ANKR/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.012, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.012", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -1401,7 +1707,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "65.0" + "cum": "25.0" } }, { @@ -1417,7 +1723,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "690.0" + "cum": "650.0" } }, { @@ -1433,7 +1739,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5690.0" + "cum": "5650.0" } }, { @@ -1449,40 +1755,40 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11940.0" + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386940.0" + "cum": "386900.0" } } ], - "ANT/USDT": [ + "ALPHA/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -1492,216 +1798,36 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "APE/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 8000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "APE/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", + "notionalCap": "200000", + "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "4500.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 200000.0, "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, @@ -1709,9 +1835,205 @@ "bracket": "4", "initialLeverage": "5", "notionalCap": "500000", - "notionalFloor": "250000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "17000.0" + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "398150.0" + } + } + ], + "AMB/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "AMB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" } }, { @@ -1727,29 +2049,29 @@ "notionalCap": "1000000", "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "29500.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "2000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "154500.0" + "cum": "148150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -1757,204 +2079,24 @@ "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "654500.0" + "cum": "898150.0" } } ], - "API3/USDT": [ + "ANC/BUSD:BUSD": [ { "tier": 1.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, + "maintenanceMarginRate": 0.02, "maxLeverage": 8.0, "info": { - "bracket": "3", + "bracket": "1", "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "APT/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "APT/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 21.0, - "info": { - "bracket": "1", - "initialLeverage": "21", - "notionalCap": "150000", + "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", "cum": "0.0" @@ -1962,102 +2104,86 @@ }, { "tier": 2.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "250000", - "notionalFloor": "150000", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "750.0" + "cum": "25.0" } }, { "tier": 3.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "7000.0" + "cum": "650.0" } }, { "tier": 4.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "250000", + "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "57000.0" + "cum": "5650.0" } }, { "tier": 5.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maxLeverage": 2.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "107000.0" + "cum": "11900.0" } }, { "tier": 6.0, - "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.25", - "cum": "732000.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 11000000.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "6", "initialLeverage": "1", - "notionalCap": "11000000", - "notionalFloor": "10000000", + "notionalCap": "1500000", + "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "3232000.0" + "cum": "386900.0" } } ], - "AR/USDT": [ + "ANKR/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -2094,18 +2220,132 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" } }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795700.0" + } + } + ], + "ANT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, { "tier": 4.0, "currency": "USDT", @@ -2119,7 +2359,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -2135,37 +2375,509 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "APE/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 10.0, + "info": { + "bracket": "1", + "initialLeverage": "10", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 8.0, + "info": { + "bracket": "2", + "initialLeverage": "8", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1200000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "APE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.0065", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.0075, + "maxLeverage": 40.0, + "info": { + "bracket": "2", + "initialLeverage": "40", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.0075", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "67.5" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "150000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "817.5" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 150000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "250000", + "notionalFloor": "150000", + "maintMarginRatio": "0.05", + "cum": "4567.5" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.1", + "cum": "17067.5" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "29567.5" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "154567.5" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.5", + "cum": "1404567.5" + } + } + ], + "API3/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "APT/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "8000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "949400.0" } } ], - "ARPA/USDT": [ + "APT/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -2177,361 +2889,115 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "ATA/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "ATOM/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.015, "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" + "maintMarginRatio": "0.015", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "900000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" + "maintMarginRatio": "0.02", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" + "initialLeverage": "10", + "notionalCap": "1800000", + "notionalFloor": "900000", + "maintMarginRatio": "0.05", + "cum": "27150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "initialLeverage": "5", + "notionalCap": "4800000", + "notionalFloor": "1800000", + "maintMarginRatio": "0.1", + "cum": "117150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, + "minNotional": 4800000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "AUCTION/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "4800000", + "maintMarginRatio": "0.125", + "cum": "237150.0" } }, { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "4", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "987150.0" } }, { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "tier": 8.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "5487150.0" } } ], - "AUDIO/USDT": [ + "AR/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -2629,346 +3095,1224 @@ } } ], - "AVAX/BUSD": [ + "ARB/USDT:USDT": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "50", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.006", "cum": "0.0" } }, { "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "AVAX/USDT": [ - { - "tier": 1.0, "currency": "USDT", - "minNotional": 0.0, + "minNotional": 5000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 25.0, "info": { - "bracket": "1", + "bracket": "2", "initialLeverage": "25", "notionalCap": "50000", - "notionalFloor": "0", + "notionalFloor": "5000", "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "250000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.05", - "cum": "7000.0" + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "57000.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" } }, { "tier": 5.0, "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", "minNotional": 2000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", "notionalCap": "5000000", "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "107000.0" + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3725770.0" + } + } + ], + "ARPA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "6000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "732000.0" + "cum": "320650.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "3232000.0" + "cum": "1820650.0" } } ], - "AXS/USDT": [ + "ASTR/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", + "initialLeverage": "20", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "250000", - "notionalFloor": "50000", - "maintMarginRatio": "0.02", - "cum": "500.0" + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "ATA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "ATOM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "600000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "600000", + "maintMarginRatio": "0.05", + "cum": "15770.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1200000.0, + "maxNotional": 3200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "3200000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "75770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3200000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "3200000", + "maintMarginRatio": "0.125", + "cum": "155770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "780770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3780770.0" + } + } + ], + "AUCTION/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "AUDIO/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "AVAX/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "4000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "949400.0" + } + } + ], + "AVAX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.005", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 40.0, + "info": { + "bracket": "2", + "initialLeverage": "40", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.0065", + "cum": "7.5" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 10000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.0075, + "maxLeverage": 30.0, + "info": { + "bracket": "3", + "initialLeverage": "30", + "notionalCap": "25000", + "notionalFloor": "10000", + "maintMarginRatio": "0.0075", + "cum": "17.5" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "4", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "80.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "5", + "initialLeverage": "20", + "notionalCap": "250000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "830.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "3", + "bracket": "6", "initialLeverage": "10", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8000.0" + "cum": "7080.0" } }, { - "tier": 4.0, + "tier": 7.0, "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "7", "initialLeverage": "5", "notionalCap": "2000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58000.0" + "cum": "57080.0" } }, { - "tier": 5.0, + "tier": 8.0, "currency": "USDT", "minNotional": 2000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "8", "initialLeverage": "4", "notionalCap": "5000000", "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "108000.0" + "cum": "107080.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "20000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "732080.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 20000000.0, + "maxNotional": 50000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "10", + "initialLeverage": "1", + "notionalCap": "50000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.5", + "cum": "5732080.0" + } + } + ], + "AXS/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.007, + "maxLeverage": 30.0, + "info": { + "bracket": "2", + "initialLeverage": "30", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.007", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "80.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "600000", + "notionalFloor": "400000", + "maintMarginRatio": "0.02", + "cum": "4080.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "600000", + "maintMarginRatio": "0.05", + "cum": "22080.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.1665, - "maxLeverage": 3.0, + "minNotional": 1200000.0, + "maxNotional": 3200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.1665", - "cum": "315500.0" + "initialLeverage": "5", + "notionalCap": "3200000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "82080.0" } }, { "tier": 7.0, "currency": "USDT", + "minNotional": 3200000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "3200000", + "maintMarginRatio": "0.125", + "cum": "162080.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "8", + "initialLeverage": "3", + "notionalCap": "10000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.15", + "cum": "287080.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", "minNotional": 10000000.0, "maxNotional": 15000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "7", + "bracket": "9", "initialLeverage": "2", "notionalCap": "15000000", "notionalFloor": "10000000", "maintMarginRatio": "0.25", - "cum": "1150500.0" + "cum": "1287080.0" } }, { - "tier": 8.0, + "tier": 10.0, "currency": "USDT", "minNotional": 15000000.0, "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "8", + "bracket": "10", "initialLeverage": "1", "notionalCap": "20000000", "notionalFloor": "15000000", "maintMarginRatio": "0.5", - "cum": "4900500.0" + "cum": "5037080.0" } } ], - "BAKE/USDT": [ + "BAKE/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -2985,7 +4329,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -3001,7 +4345,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -3017,7 +4361,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -3033,27 +4377,27 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "BAL/USDT": [ + "BAL/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -3151,89 +4495,105 @@ } } ], - "BAND/USDT": [ + "BAND/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", - "notionalCap": "25000", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, "maxLeverage": 10.0, "info": { "bracket": "2", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" + "cum": "5650.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "11900.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "1", "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "386900.0" } } ], - "BAT/USDT": [ + "BAT/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -3331,166 +4691,182 @@ } } ], - "BCH/USDT": [ + "BCH/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 50.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", - "notionalCap": "10000", + "initialLeverage": "75", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.0065", + "cum": "7.5" + } + }, + { + "tier": 3.0, + "currency": "USDT", "minNotional": 10000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 40.0, "info": { - "bracket": "2", + "bracket": "3", "initialLeverage": "40", "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "35.0" + "cum": "42.5" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "25", "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "535.0" + "cum": "542.5" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "10", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8035.0" + "cum": "8042.5" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "5", "notionalCap": "2000000", "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "58042.5" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", "minNotional": 2000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "4", "notionalCap": "5000000", "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "108035.0" + "cum": "108042.5" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", "minNotional": 5000000.0, "maxNotional": 10000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "3", "notionalCap": "10000000", "notionalFloor": "5000000", "maintMarginRatio": "0.15", - "cum": "233035.0" + "cum": "233042.5" } }, { - "tier": 8.0, + "tier": 9.0, "currency": "USDT", "minNotional": 10000000.0, "maxNotional": 20000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "8", + "bracket": "9", "initialLeverage": "2", "notionalCap": "20000000", "notionalFloor": "10000000", "maintMarginRatio": "0.25", - "cum": "1233035.0" + "cum": "1233042.5" } }, { - "tier": 9.0, + "tier": 10.0, "currency": "USDT", "minNotional": 20000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "9", + "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", "notionalFloor": "20000000", "maintMarginRatio": "0.5", - "cum": "6233035.0" + "cum": "6233042.5" } } ], - "BEL/USDT": [ + "BEL/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -3507,7 +4883,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -3523,7 +4899,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -3539,7 +4915,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -3555,27 +4931,27 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 8000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "8000000", + "notionalCap": "2000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "BLUEBIRD/USDT": [ + "BLUEBIRD/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -3673,20 +5049,134 @@ } } ], - "BLZ/USDT": [ + "BLUR/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "BLZ/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -3703,7 +5193,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -3719,7 +5209,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -3735,7 +5225,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -3751,27 +5241,27 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "2000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "BNB/BUSD": [ + "BNB/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", @@ -3869,101 +5359,101 @@ } } ], - "BNB/USDT": [ + "BNB/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 50.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", - "notionalCap": "10000", + "initialLeverage": "75", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", "minNotional": 10000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 40.0, "info": { - "bracket": "2", + "bracket": "3", "initialLeverage": "40", "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "35.0" + "cum": "45.0" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "25", "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "535.0" + "cum": "545.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "10", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8035.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "5", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 1000000.0, "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "4", + "initialLeverage": "5", "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "108035.0" + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "58045.0" } }, { @@ -3971,15 +5461,15 @@ "currency": "USDT", "minNotional": 5000000.0, "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "7", - "initialLeverage": "3", + "initialLeverage": "4", "notionalCap": "10000000", "notionalFloor": "5000000", - "maintMarginRatio": "0.15", - "cum": "233035.0" + "maintMarginRatio": "0.125", + "cum": "183045.0" } }, { @@ -3987,159 +5477,159 @@ "currency": "USDT", "minNotional": 10000000.0, "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, "info": { "bracket": "8", - "initialLeverage": "2", + "initialLeverage": "3", "notionalCap": "20000000", "notionalFloor": "10000000", - "maintMarginRatio": "0.25", - "cum": "1233035.0" + "maintMarginRatio": "0.15", + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2433045.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "9", + "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233035.0" + "cum": "9933045.0" } } ], - "BNX/USDT": [ + "BNX/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", + "initialLeverage": "20", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "750.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "minNotional": 25000.0, + "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", + "notionalCap": "100000", + "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "4500.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, + "minNotional": 100000.0, + "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", + "notionalCap": "250000", + "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "17000.0" + "cum": "5650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 500000.0, + "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maxLeverage": 2.0, "info": { "bracket": "5", - "initialLeverage": "4", + "initialLeverage": "2", "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "29500.0" + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.25", - "cum": "154500.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 8000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "6", "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "2000000", + "notionalCap": "5000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "654500.0" + "cum": "386900.0" } } ], - "BTC/BUSD": [ + "BTC/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.004, - "maxLeverage": 50.0, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", + "initialLeverage": "75", "notionalCap": "50000", "notionalFloor": "0", "maintMarginRatio": "0.004", @@ -4152,10 +5642,10 @@ "minNotional": 50000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.005, - "maxLeverage": 25.0, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.005", @@ -4168,10 +5658,10 @@ "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.01", @@ -4184,10 +5674,10 @@ "minNotional": 1000000.0, "maxNotional": 7500000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "7500000", "notionalFloor": "1000000", "maintMarginRatio": "0.025", @@ -4200,10 +5690,10 @@ "minNotional": 7500000.0, "maxNotional": 40000000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "6", + "initialLeverage": "10", "notionalCap": "40000000", "notionalFloor": "7500000", "maintMarginRatio": "0.05", @@ -4291,7 +5781,7 @@ } } ], - "BTC/USDT": [ + "BTC/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -4328,13 +5818,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 250000.0, - "maxNotional": 1000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maxLeverage": 50.0, "info": { "bracket": "3", - "initialLeverage": "25", - "notionalCap": "1000000", + "initialLeverage": "50", + "notionalCap": "3000000", "notionalFloor": "250000", "maintMarginRatio": "0.01", "cum": "1300.0" @@ -4343,55 +5833,55 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 10000000.0, + "minNotional": 3000000.0, + "maxNotional": 15000000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "15", - "notionalCap": "10000000", - "notionalFloor": "1000000", + "initialLeverage": "20", + "notionalCap": "15000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.025", - "cum": "16300.0" + "cum": "46300.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 15000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "15000000", "maintMarginRatio": "0.05", - "cum": "266300.0" + "cum": "421300.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 50000000.0, + "minNotional": 30000000.0, + "maxNotional": 80000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalCap": "80000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.1", - "cum": "1266300.0" + "cum": "1921300.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 50000000.0, + "minNotional": 80000000.0, "maxNotional": 100000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, @@ -4399,9 +5889,9 @@ "bracket": "7", "initialLeverage": "4", "notionalCap": "100000000", - "notionalFloor": "50000000", + "notionalFloor": "80000000", "maintMarginRatio": "0.125", - "cum": "2516300.0" + "cum": "3921300.0" } }, { @@ -4417,7 +5907,7 @@ "notionalCap": "200000000", "notionalFloor": "100000000", "maintMarginRatio": "0.15", - "cum": "5016300.0" + "cum": "6421300.0" } }, { @@ -4433,7 +5923,7 @@ "notionalCap": "300000000", "notionalFloor": "200000000", "maintMarginRatio": "0.25", - "cum": "2.50163E7" + "cum": "2.64213E7" } }, { @@ -4449,207 +5939,11 @@ "notionalCap": "500000000", "notionalFloor": "300000000", "maintMarginRatio": "0.5", - "cum": "1.000163E8" + "cum": "1.014213E8" } } ], - "BTCDOM/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "BTCSTUSDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 9.223372036854776e+18, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "9223372036854775807", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "BTCUSDT_221230": [ + "BTC/USDT:USDT-230630": [ { "tier": 1.0, "currency": "USDT", @@ -4763,7 +6057,203 @@ } } ], - "BTS/USDT": [ + "BTCDOM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "BTCST/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 9.223372036854776e+18, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "9223372036854775807", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "BTS/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -4861,20 +6351,20 @@ } } ], - "C98/USDT": [ + "C98/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "1", "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -4891,186 +6381,104 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "35650.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "CELO/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "910650.0" } } ], - "CELR/USDT": [ + "CELO/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "1", "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -5087,91 +6495,351 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "35650.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "910650.0" } } ], - "CHR/USDT": [ + "CELR/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "CFX/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", "cum": "0.0" } }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.015", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "300000", + "notionalFloor": "25000", + "maintMarginRatio": "0.02", + "cum": "150.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "300000", + "maintMarginRatio": "0.05", + "cum": "9150.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1200000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "3000000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "69150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "144150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "894150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "18000000", + "maintMarginRatio": "0.5", + "cum": "5394150.0" + } + } + ], + "CHR/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, { "tier": 2.0, "currency": "USDT", @@ -5185,45 +6853,45 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, @@ -5231,9 +6899,9 @@ "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23150.0" } }, { @@ -5249,11 +6917,11 @@ "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "398150.0" } } ], - "CHZ/USDT": [ + "CHZ/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -5351,7 +7019,219 @@ } } ], - "COMP/USDT": [ + "CKB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "COCOS/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "COMP/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -5449,20 +7329,20 @@ } } ], - "COTI/USDT": [ + "COTI/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "1", "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -5479,45 +7359,45 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, @@ -5525,9 +7405,9 @@ "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23150.0" } }, { @@ -5543,21 +7423,21 @@ "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "398150.0" } } ], - "CRV/USDT": [ + "CRV/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 21.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "21", + "initialLeverage": "25", "notionalCap": "50000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -5632,13 +7512,13 @@ "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "2000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", "cum": "154500.0" @@ -5647,34 +7527,132 @@ { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 3000000.0, + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "2000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "654500.0" + "cum": "904500.0" } } ], - "CTK/USDT": [ + "CTK/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "CTSI/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "1", "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -5691,153 +7669,55 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "CTSI/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "35650.0" } }, { @@ -5845,199 +7725,215 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "910650.0" } } ], - "CVC/USDT": [ + "CVC/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", + "initialLeverage": "10", + "notionalCap": "25000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.025", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 8.0, "info": { - "bracket": "3", + "bracket": "2", "initialLeverage": "8", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "625.0" } }, { - "tier": 4.0, + "tier": 3.0, "currency": "USDT", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "3", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5625.0" } }, { - "tier": 5.0, + "tier": 4.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "5", + "bracket": "4", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11875.0" } }, { - "tier": 6.0, + "tier": 5.0, "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "5", "initialLeverage": "1", "notionalCap": "2000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386875.0" } } ], - "CVX/BUSD": [ + "CVX/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "10", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "initialLeverage": "8", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "5650.0" } }, { "tier": 5.0, "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 250000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 500000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "1000000", + "notionalFloor": "500000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "199400.0" } } ], - "CVX/USDT": [ + "CVX/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -6119,20 +8015,20 @@ } } ], - "DAR/USDT": [ + "DAR/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -6142,82 +8038,98 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820650.0" } } ], - "DASH/USDT": [ + "DASH/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -6315,7 +8227,7 @@ } } ], - "DEFI/USDT": [ + "DEFI/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -6413,20 +8325,20 @@ } } ], - "DENT/USDT": [ + "DENT/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "1", "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -6443,88 +8355,104 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "35650.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "910650.0" } } ], - "DGB/USDT": [ + "DGB/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -6541,7 +8469,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -6557,7 +8485,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -6573,7 +8501,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -6589,7 +8517,7 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { @@ -6605,93 +8533,109 @@ "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "DODO/BUSD": [ + "DODO/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", - "notionalCap": "25000", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, "maxLeverage": 10.0, "info": { "bracket": "2", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" + "cum": "5650.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "BUSD", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "11900.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "386900.0" } } ], - "DOGE/BUSD": [ + "DOGE/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", @@ -6789,203 +8733,267 @@ } } ], - "DOGE/USDT": [ + "DOGE/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", + "initialLeverage": "50", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.006", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.007, + "maxLeverage": 40.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "250000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" + "initialLeverage": "40", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.007", + "cum": "5.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.05", - "cum": "7000.0" + "initialLeverage": "25", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "80.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 600000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "57000.0" + "initialLeverage": "20", + "notionalCap": "900000", + "notionalFloor": "600000", + "maintMarginRatio": "0.025", + "cum": "9080.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "107000.0" + "initialLeverage": "10", + "notionalCap": "1800000", + "notionalFloor": "900000", + "maintMarginRatio": "0.05", + "cum": "31580.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.25", - "cum": "732000.0" + "initialLeverage": "5", + "notionalCap": "4800000", + "notionalFloor": "1800000", + "maintMarginRatio": "0.1", + "cum": "121580.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 4800000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "4800000", + "maintMarginRatio": "0.125", + "cum": "241580.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "991580.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "9", "initialLeverage": "1", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "3232000.0" + "cum": "5491580.0" } } ], - "DOT/BUSD": [ + "DOT/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "10", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "initialLeverage": "8", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "5650.0" } }, { "tier": 5.0, "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 30000000.0, + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "30000000", - "notionalFloor": "1000000", + "notionalCap": "4000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "949400.0" } } ], - "DOT/USDT": [ + "DOT/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -7131,20 +9139,20 @@ } } ], - "DUSK/USDT": [ + "DUSK/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -7154,134 +9162,36 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "DYDX/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", + "notionalCap": "200000", + "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "4500.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 200000.0, "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, @@ -7289,9 +9199,9 @@ "bracket": "4", "initialLeverage": "5", "notionalCap": "500000", - "notionalFloor": "250000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "17000.0" + "cum": "10650.0" } }, { @@ -7307,56 +9217,186 @@ "notionalCap": "1000000", "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "29500.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 4000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "4000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "154500.0" + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "DYDX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "300.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10300.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50300.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100300.0" } }, { "tier": 7.0, "currency": "USDT", "minNotional": 4000000.0, - "maxNotional": 8000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.25", + "cum": "600300.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "4000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "1154500.0" + "cum": "3600300.0" } } ], - "EGLD/USDT": [ + "EDU/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -7366,134 +9406,36 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 50000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "50000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "ENJ/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "250000", - "notionalFloor": "150000", - "maintMarginRatio": "0.05", - "cum": "4500.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 200000.0, "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, @@ -7501,9 +9443,9 @@ "bracket": "4", "initialLeverage": "5", "notionalCap": "500000", - "notionalFloor": "250000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "17000.0" + "cum": "10650.0" } }, { @@ -7519,43 +9461,43 @@ "notionalCap": "1000000", "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "29500.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "2000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "154500.0" + "cum": "148150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 3000000.0, + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "2000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "654500.0" + "cum": "898150.0" } } ], - "ENS/USDT": [ + "EGLD/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -7592,12 +9534,256 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820700.0" + } + } + ], + "ENJ/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "100000", + "notionalFloor": "50000", + "maintMarginRatio": "0.02", + "cum": "500.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "150000", + "notionalFloor": "100000", + "maintMarginRatio": "0.025", + "cum": "1000.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 150000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "4", + "initialLeverage": "8", + "notionalCap": "250000", + "notionalFloor": "150000", + "maintMarginRatio": "0.05", + "cum": "4750.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.1", + "cum": "17250.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "29750.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "154750.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.5", + "cum": "1404750.0" + } + } + ], + "ENS/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -7653,101 +9839,101 @@ } } ], - "EOS/USDT": [ + "EOS/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 50.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", - "notionalCap": "10000", + "initialLeverage": "75", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", "minNotional": 10000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 40.0, "info": { - "bracket": "2", + "bracket": "3", "initialLeverage": "40", "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "35.0" + "cum": "45.0" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "25", "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "535.0" + "cum": "545.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "10", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8035.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "5", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 1000000.0, "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "4", + "initialLeverage": "5", "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "108035.0" + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "58045.0" } }, { @@ -7755,15 +9941,15 @@ "currency": "USDT", "minNotional": 5000000.0, "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "7", - "initialLeverage": "3", + "initialLeverage": "4", "notionalCap": "10000000", "notionalFloor": "5000000", - "maintMarginRatio": "0.15", - "cum": "233035.0" + "maintMarginRatio": "0.125", + "cum": "183045.0" } }, { @@ -7771,211 +9957,243 @@ "currency": "USDT", "minNotional": 10000000.0, "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, "info": { "bracket": "8", - "initialLeverage": "2", + "initialLeverage": "3", "notionalCap": "20000000", "notionalFloor": "10000000", - "maintMarginRatio": "0.25", - "cum": "1233035.0" + "maintMarginRatio": "0.15", + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2433045.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "9", + "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233035.0" + "cum": "9933045.0" } } ], - "ETC/BUSD": [ + "ETC/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "8", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" + "cum": "5650.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "BUSD", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "11900.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "386900.0" } } ], - "ETC/USDT": [ + "ETC/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 50.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", - "notionalCap": "10000", + "initialLeverage": "75", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", "minNotional": 10000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 40.0, "info": { - "bracket": "2", + "bracket": "3", "initialLeverage": "40", "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "35.0" + "cum": "45.0" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "25", "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "535.0" + "cum": "545.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "10", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8035.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "5", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 1000000.0, "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "4", + "initialLeverage": "5", "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "108035.0" + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "58045.0" } }, { @@ -7983,15 +10201,15 @@ "currency": "USDT", "minNotional": 5000000.0, "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "7", - "initialLeverage": "3", + "initialLeverage": "4", "notionalCap": "10000000", "notionalFloor": "5000000", - "maintMarginRatio": "0.15", - "cum": "233035.0" + "maintMarginRatio": "0.125", + "cum": "183045.0" } }, { @@ -7999,45 +10217,223 @@ "currency": "USDT", "minNotional": 10000000.0, "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, "info": { "bracket": "8", - "initialLeverage": "2", + "initialLeverage": "3", "notionalCap": "20000000", "notionalFloor": "10000000", - "maintMarginRatio": "0.25", - "cum": "1233035.0" + "maintMarginRatio": "0.15", + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2433045.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "9", + "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233035.0" + "cum": "9933045.0" } } ], - "ETH/BUSD": [ + "ETH/BTC:BTC": [ + { + "tier": 1.0, + "currency": "BTC", + "minNotional": 0.0, + "maxNotional": 5.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "5", + "notionalFloor": "0", + "maintMarginRatio": "0.005", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BTC", + "minNotional": 5.0, + "maxNotional": 10.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10", + "notionalFloor": "5", + "maintMarginRatio": "0.006", + "cum": "0.005" + } + }, + { + "tier": 3.0, + "currency": "BTC", + "minNotional": 10.0, + "maxNotional": 100.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "100", + "notionalFloor": "10", + "maintMarginRatio": "0.01", + "cum": "0.045" + } + }, + { + "tier": 4.0, + "currency": "BTC", + "minNotional": 100.0, + "maxNotional": 250.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "250", + "notionalFloor": "100", + "maintMarginRatio": "0.02", + "cum": "1.045" + } + }, + { + "tier": 5.0, + "currency": "BTC", + "minNotional": 250.0, + "maxNotional": 800.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "800", + "notionalFloor": "250", + "maintMarginRatio": "0.025", + "cum": "2.295" + } + }, + { + "tier": 6.0, + "currency": "BTC", + "minNotional": 800.0, + "maxNotional": 1500.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "6", + "initialLeverage": "8", + "notionalCap": "1500", + "notionalFloor": "800", + "maintMarginRatio": "0.05", + "cum": "22.295" + } + }, + { + "tier": 7.0, + "currency": "BTC", + "minNotional": 1500.0, + "maxNotional": 2000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "7", + "initialLeverage": "5", + "notionalCap": "2000", + "notionalFloor": "1500", + "maintMarginRatio": "0.1", + "cum": "97.295" + } + }, + { + "tier": 8.0, + "currency": "BTC", + "minNotional": 2000.0, + "maxNotional": 3000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "8", + "initialLeverage": "4", + "notionalCap": "3000", + "notionalFloor": "2000", + "maintMarginRatio": "0.125", + "cum": "147.295" + } + }, + { + "tier": 9.0, + "currency": "BTC", + "minNotional": 3000.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "5000", + "notionalFloor": "3000", + "maintMarginRatio": "0.25", + "cum": "522.295" + } + }, + { + "tier": 10.0, + "currency": "BTC", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "10", + "initialLeverage": "1", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.5", + "cum": "1772.295" + } + } + ], + "ETH/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.004, - "maxLeverage": 50.0, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", + "initialLeverage": "75", "notionalCap": "50000", "notionalFloor": "0", "maintMarginRatio": "0.004", @@ -8050,10 +10446,10 @@ "minNotional": 50000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.005, - "maxLeverage": 25.0, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "100000", "notionalFloor": "50000", "maintMarginRatio": "0.005", @@ -8066,10 +10462,10 @@ "minNotional": 100000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "1000000", "notionalFloor": "100000", "maintMarginRatio": "0.01", @@ -8082,10 +10478,10 @@ "minNotional": 1000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.025", @@ -8098,10 +10494,10 @@ "minNotional": 5000000.0, "maxNotional": 10000000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "6", + "initialLeverage": "10", "notionalCap": "10000000", "notionalFloor": "5000000", "maintMarginRatio": "0.05", @@ -8189,7 +10585,7 @@ } } ], - "ETH/USDT": [ + "ETH/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -8226,13 +10622,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 250000.0, - "maxNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maxLeverage": 50.0, "info": { "bracket": "3", - "initialLeverage": "25", - "notionalCap": "1000000", + "initialLeverage": "50", + "notionalCap": "2000000", "notionalFloor": "250000", "maintMarginRatio": "0.01", "cum": "1025.0" @@ -8241,71 +10637,71 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "15", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "initialLeverage": "20", + "notionalCap": "10000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.02", - "cum": "11025.0" + "cum": "21025.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 10000000.0, + "maxNotional": 25000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "25000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.05", - "cum": "161025.0" + "cum": "321025.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, + "minNotional": 25000000.0, + "maxNotional": 50000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalCap": "50000000", + "notionalFloor": "25000000", "maintMarginRatio": "0.1", - "cum": "661025.0" + "cum": "1571025.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 40000000.0, + "minNotional": 50000000.0, + "maxNotional": 60000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "40000000", - "notionalFloor": "20000000", + "notionalCap": "60000000", + "notionalFloor": "50000000", "maintMarginRatio": "0.125", - "cum": "1161025.0" + "cum": "2821025.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 40000000.0, + "minNotional": 60000000.0, "maxNotional": 80000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, @@ -8313,9 +10709,9 @@ "bracket": "8", "initialLeverage": "3", "notionalCap": "80000000", - "notionalFloor": "40000000", + "notionalFloor": "60000000", "maintMarginRatio": "0.15", - "cum": "2161025.0" + "cum": "4321025.0" } }, { @@ -8331,7 +10727,7 @@ "notionalCap": "150000000", "notionalFloor": "80000000", "maintMarginRatio": "0.25", - "cum": "1.0161025E7" + "cum": "1.2321025E7" } }, { @@ -8347,11 +10743,11 @@ "notionalCap": "300000000", "notionalFloor": "150000000", "maintMarginRatio": "0.5", - "cum": "4.7661025E7" + "cum": "4.9821025E7" } } ], - "ETHUSDT_221230": [ + "ETH/USDT:USDT-230630": [ { "tier": 1.0, "currency": "USDT", @@ -8465,647 +10861,59 @@ } } ], - "FIL/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "FIL/USDT": [ + "FET/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 250000.0, + "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "250000", - "notionalFloor": "50000", - "maintMarginRatio": "0.02", - "cum": "500.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.05", - "cum": "8000.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "58000.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "108000.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.1665, - "maxLeverage": 3.0, - "info": { - "bracket": "6", - "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.1665", - "cum": "315500.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "7", - "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", - "maintMarginRatio": "0.25", - "cum": "1150500.0" - } - }, - { - "tier": 8.0, - "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 30000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "8", - "initialLeverage": "1", - "notionalCap": "30000000", - "notionalFloor": "20000000", - "maintMarginRatio": "0.5", - "cum": "6150500.0" - } - } - ], - "FLM/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "FLOW/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "FOOTBALL/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "FTM/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "FTM/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, "maxLeverage": 25.0, "info": { "bracket": "1", "initialLeverage": "25", - "notionalCap": "50000", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "2", "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", + "notionalCap": "25000", + "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "750.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "minNotional": 25000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", + "notionalCap": "200000", + "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "4500.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 200000.0, "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, @@ -9113,9 +10921,9 @@ "bracket": "4", "initialLeverage": "5", "notionalCap": "500000", - "notionalFloor": "250000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "17000.0" + "cum": "10650.0" } }, { @@ -9131,29 +10939,681 @@ "notionalCap": "1000000", "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "29500.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 4000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "4000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "154500.0" + "cum": "148150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 4000000.0, + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "FIL/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "FIL/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "50000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "250000", + "notionalFloor": "50000", + "maintMarginRatio": "0.01", + "cum": "200.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "600000", + "notionalFloor": "250000", + "maintMarginRatio": "0.02", + "cum": "2700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "600000", + "maintMarginRatio": "0.05", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1200000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "3000000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "80700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "155700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.165, + "maxLeverage": 3.0, + "info": { + "bracket": "7", + "initialLeverage": "3", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.165", + "cum": "395700.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.25", + "cum": "1245700.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.5", + "cum": "6245700.0" + } + } + ], + "FLM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "FLOW/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "FOOTBALL/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "FTM/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -9161,13 +11621,143 @@ "bracket": "7", "initialLeverage": "1", "notionalCap": "8000000", - "notionalFloor": "4000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "1154500.0" + "cum": "949400.0" } } ], - "FTT/BUSD": [ + "FTM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3725770.0" + } + } + ], + "FTT/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", @@ -9265,7 +11855,7 @@ } } ], - "FTT/USDT": [ + "FTT/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -9347,102 +11937,20 @@ } } ], - "GAL/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "GAL/USDT": [ + "FXS/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -9452,14 +11960,14 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -9468,199 +11976,117 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "GAL/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 10.0, + "info": { + "bracket": "1", + "initialLeverage": "10", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, "maxLeverage": 8.0, "info": { - "bracket": "3", + "bracket": "2", "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "GALA/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "GALA/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "10", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, @@ -9671,350 +12097,56 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "GMT/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "5650.0" } }, { "tier": 5.0, "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 8000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "GMT/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", - "maintMarginRatio": "0.05", - "cum": "4500.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", "minNotional": 250000.0, "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, "info": { - "bracket": "4", - "initialLeverage": "5", + "bracket": "5", + "initialLeverage": "2", "notionalCap": "500000", "notionalFloor": "250000", - "maintMarginRatio": "0.1", - "cum": "17000.0" + "maintMarginRatio": "0.125", + "cum": "11900.0" } }, { - "tier": 5.0, - "currency": "USDT", + "tier": 6.0, + "currency": "BUSD", "minNotional": 500000.0, "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, "info": { - "bracket": "5", - "initialLeverage": "4", + "bracket": "6", + "initialLeverage": "1", "notionalCap": "1000000", "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "29500.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.25", - "cum": "154500.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 30000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "30000000", - "notionalFloor": "2000000", "maintMarginRatio": "0.5", - "cum": "654500.0" + "cum": "199400.0" } } ], - "GRT/USDT": [ + "GAL/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "GTC/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -10031,7 +12163,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -10047,7 +12179,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -10063,7 +12195,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -10079,579 +12211,7 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "HBAR/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "HNT/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "HOT/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "ICP/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 30000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "30000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "ICP/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 30000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "30000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "ICX/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { @@ -10667,30 +12227,30 @@ "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "IMX/USDT": [ + "GALA/BUSD:BUSD": [ { "tier": 1.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, @@ -10701,12 +12261,12 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, @@ -10717,12 +12277,12 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, @@ -10733,12 +12293,12 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { "tier": 5.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, @@ -10749,12 +12309,12 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { "tier": 6.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 1000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, @@ -10765,4375 +12325,225 @@ "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "INJ/USDT": [ + "GALA/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "IOST/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "IOTA/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "IOTX/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "JASMY/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "KAVA/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "KLAY/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "KNC/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "KSM/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "LDO/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "LDO/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "LEVER/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "LINA/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "LINK/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "LINK/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, + "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", - "notionalCap": "10000", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.006", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 10000.0, + "minNotional": 5000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 40.0, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "40", + "initialLeverage": "25", "notionalCap": "50000", - "notionalFloor": "10000", + "notionalFloor": "5000", "maintMarginRatio": "0.01", - "cum": "35.0" + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "25", - "notionalCap": "250000", + "initialLeverage": "20", + "notionalCap": "900000", "notionalFloor": "50000", - "maintMarginRatio": "0.02", - "cum": "535.0" + "maintMarginRatio": "0.025", + "cum": "770.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 900000.0, + "maxNotional": 1800000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalCap": "1800000", + "notionalFloor": "900000", "maintMarginRatio": "0.05", - "cum": "8035.0" + "cum": "23270.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 1800000.0, + "maxNotional": 4800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "4800000", + "notionalFloor": "1800000", "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "113270.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 4800000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "6000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.125", - "cum": "108035.0" + "cum": "233270.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, "info": { "bracket": "7", - "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.15", - "cum": "233035.0" + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "983270.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 18000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, "info": { "bracket": "8", - "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", - "maintMarginRatio": "0.25", - "cum": "1233035.0" - } - }, - { - "tier": 9.0, - "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 50000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "9", "initialLeverage": "1", - "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "6233035.0" + "cum": "5483270.0" } } ], - "LIT/USDT": [ + "GMT/BUSD:BUSD": [ { "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "LPT/USDT": [ - { - "tier": 1.0, - "currency": "USDT", + "currency": "BUSD", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "LRC/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "LTC/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "LTC/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "10000", - "notionalFloor": "0", - "maintMarginRatio": "0.0065", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 10000.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "50000", - "notionalFloor": "10000", - "maintMarginRatio": "0.01", - "cum": "35.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 250000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 15.0, + "maxLeverage": 8.0, "info": { - "bracket": "3", - "initialLeverage": "15", - "notionalCap": "250000", - "notionalFloor": "50000", + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", "maintMarginRatio": "0.02", - "cum": "535.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "4", - "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.05", - "cum": "8035.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "5", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "58035.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "6", - "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "108035.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "7", - "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.15", - "cum": "233035.0" - } - }, - { - "tier": 8.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "8", - "initialLeverage": "2", - "notionalCap": "20000000", - "notionalFloor": "10000000", - "maintMarginRatio": "0.25", - "cum": "1233035.0" - } - }, - { - "tier": 9.0, - "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 32000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "9", - "initialLeverage": "1", - "notionalCap": "32000000", - "notionalFloor": "20000000", - "maintMarginRatio": "0.5", - "cum": "6233035.0" - } - } - ], - "LUNA2/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", "cum": "0.0" } }, { "tier": 2.0, "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 8000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "LUNA2/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.015, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.015", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "50.0" + "cum": "25.0" } }, { "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11925.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386925.0" - } - } - ], - "MANA/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", - "maintMarginRatio": "0.05", - "cum": "4500.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.1", - "cum": "17000.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "29500.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.25", - "cum": "154500.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.5", - "cum": "654500.0" - } - } - ], - "MASK/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "MATIC/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, "currency": "BUSD", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 6.0, "info": { - "bracket": "2", - "initialLeverage": "10", + "bracket": "3", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "625.0" + "cum": "650.0" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" + "cum": "5650.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "BUSD", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "11900.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "MATIC/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", - "maintMarginRatio": "0.05", - "cum": "4500.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.1", - "cum": "17000.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 750000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "750000", - "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "29500.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 750000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "750000", - "maintMarginRatio": "0.25", - "cum": "123250.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 8000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "373250.0" - } - } - ], - "MKR/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "MTL/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "NEAR/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "NEAR/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "50000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 150000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "150000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "750.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "250000", - "notionalFloor": "150000", - "maintMarginRatio": "0.05", - "cum": "4500.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.1", - "cum": "17000.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "29500.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.25", - "cum": "154500.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.5", - "cum": "654500.0" - } - } - ], - "NEO/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "NKN/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "OCEAN/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "OGN/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "OMG/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "ONE/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "ONT/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "OP/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 8000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "PEOPLE/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "PHB/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "QNT/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "QTUM/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "RAY/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "REEF/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "REN/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -15143,106 +12553,366 @@ "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "RLC/USDT": [ + "GMT/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "50000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.01", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 50000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "initialLeverage": "20", + "notionalCap": "150000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "750.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 100000.0, + "minNotional": 150000.0, "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "5", + "initialLeverage": "10", "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" + "notionalFloor": "150000", + "maintMarginRatio": "0.05", + "cum": "4500.0" } }, { "tier": 4.0, "currency": "USDT", "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", + "initialLeverage": "5", + "notionalCap": "500000", "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" + "maintMarginRatio": "0.1", + "cum": "17000.0" } }, { "tier": 5.0, "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "29500.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "154500.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "30000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "654500.0" } } ], - "ROSE/USDT": [ + "GMX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 480000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "480000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 480000.0, + "maxNotional": 1280000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1280000", + "notionalFloor": "480000", + "maintMarginRatio": "0.1", + "cum": "24650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1280000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1600000", + "notionalFloor": "1280000", + "maintMarginRatio": "0.125", + "cum": "56650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "4800000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.25", + "cum": "256650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 4800000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "4800000", + "maintMarginRatio": "0.5", + "cum": "1456650.0" + } + } + ], + "GRT/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795700.0" + } + } + ], + "GTC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -15259,7 +12929,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -15275,6 +12945,104 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "HBAR/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", "cum": "700.0" } }, @@ -15327,7 +13095,447 @@ } } ], - "RSR/USDT": [ + "HFT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "HIGH/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795650.0" + } + } + ], + "HNT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 10.0, + "info": { + "bracket": "1", + "initialLeverage": "10", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 8.0, + "info": { + "bracket": "2", + "initialLeverage": "8", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "300000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "800000", + "notionalFloor": "300000", + "maintMarginRatio": "0.1", + "cum": "15650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.125", + "cum": "35650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1500000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.5", + "cum": "535650.0" + } + } + ], + "HOOK/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "HOT/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -15425,7 +13633,789 @@ } } ], - "RUNE/USDT": [ + "ICP/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820700.0" + } + } + ], + "ICX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "ID/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "IDEX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "IMX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820700.0" + } + } + ], + "INJ/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 1200000.0, + "maxNotional": 3200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "3200000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "60650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 3200000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "4000000", + "notionalFloor": "3200000", + "maintMarginRatio": "0.125", + "cum": "140650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.25", + "cum": "640650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3640650.0" + } + } + ], + "IOST/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "IOTA/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -15523,7 +14513,7 @@ } } ], - "RVN/USDT": [ + "IOTX/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -15621,11 +14611,1477 @@ } } ], - "SAND/BUSD": [ + "JASMY/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "JOE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "KAVA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 10000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "10000", + "maintMarginRatio": "0.025", + "cum": "100.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "4", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "725.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5725.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11975.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "1136975.0" + } + } + ], + "KLAY/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795700.0" + } + } + ], + "KNC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "KSM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "LDO/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "LDO/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3725770.0" + } + } + ], + "LEVER/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.023, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.023", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "10.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "635.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5635.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11885.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386885.0" + } + } + ], + "LEVER/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "LINA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "900000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 900000.0, + "maxNotional": 2400000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "2400000", + "notionalFloor": "900000", + "maintMarginRatio": "0.1", + "cum": "45650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 2400000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "3000000", + "notionalFloor": "2400000", + "maintMarginRatio": "0.125", + "cum": "105650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 9000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "9000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.25", + "cum": "480650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 9000000.0, + "maxNotional": 15000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "15000000", + "notionalFloor": "9000000", + "maintMarginRatio": "0.5", + "cum": "2730650.0" + } + } + ], + "LINK/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "949400.0" + } + } + ], + "LINK/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.005", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 40.0, + "info": { + "bracket": "3", + "initialLeverage": "40", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.01", + "cum": "45.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "4", + "initialLeverage": "25", + "notionalCap": "250000", + "notionalFloor": "50000", + "maintMarginRatio": "0.02", + "cum": "545.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.05", + "cum": "8045.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "58045.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "10000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.125", + "cum": "183045.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "8", + "initialLeverage": "3", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.15", + "cum": "433045.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2433045.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 30000000.0, + "maxNotional": 50000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "10", + "initialLeverage": "1", + "notionalCap": "50000000", + "notionalFloor": "30000000", + "maintMarginRatio": "0.5", + "cum": "9933045.0" + } + } + ], + "LIT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, @@ -15640,7 +16096,7 @@ }, { "tier": 2.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, @@ -15656,7 +16112,7 @@ }, { "tier": 3.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, @@ -15672,7 +16128,7 @@ }, { "tier": 4.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, @@ -15688,7 +16144,7 @@ }, { "tier": 5.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, @@ -15703,7 +16159,1781 @@ } } ], - "SAND/USDT": [ + "LPT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "LQTY/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "LRC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "LTC/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "949400.0" + } + } + ], + "LTC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.005", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 40.0, + "info": { + "bracket": "3", + "initialLeverage": "40", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.01", + "cum": "45.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "4", + "initialLeverage": "25", + "notionalCap": "250000", + "notionalFloor": "50000", + "maintMarginRatio": "0.02", + "cum": "545.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.05", + "cum": "8045.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "58045.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "10000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.125", + "cum": "183045.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "8", + "initialLeverage": "3", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.15", + "cum": "433045.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2433045.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 30000000.0, + "maxNotional": 50000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "10", + "initialLeverage": "1", + "notionalCap": "50000000", + "notionalFloor": "30000000", + "maintMarginRatio": "0.5", + "cum": "9933045.0" + } + } + ], + "LUNA2/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11925.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386925.0" + } + } + ], + "MAGIC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "MANA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.0065", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.0075, + "maxLeverage": 40.0, + "info": { + "bracket": "2", + "initialLeverage": "40", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.0075", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "67.5" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "150000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "817.5" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 150000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "250000", + "notionalFloor": "150000", + "maintMarginRatio": "0.05", + "cum": "4567.5" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.1", + "cum": "17067.5" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "29567.5" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "154567.5" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.5", + "cum": "1404567.5" + } + } + ], + "MASK/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "900000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "175.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 900000.0, + "maxNotional": 1800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1800000", + "notionalFloor": "900000", + "maintMarginRatio": "0.05", + "cum": "22675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1800000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "4800000", + "notionalFloor": "1800000", + "maintMarginRatio": "0.1", + "cum": "112675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 4800000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "4800000", + "maintMarginRatio": "0.125", + "cum": "232675.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "982675.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "18000000", + "maintMarginRatio": "0.5", + "cum": "5482675.0" + } + } + ], + "MATIC/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "949400.0" + } + } + ], + "MATIC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.007, + "maxLeverage": 40.0, + "info": { + "bracket": "2", + "initialLeverage": "40", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.007", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "80.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "900000", + "notionalFloor": "600000", + "maintMarginRatio": "0.025", + "cum": "9080.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 900000.0, + "maxNotional": 1800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "1800000", + "notionalFloor": "900000", + "maintMarginRatio": "0.05", + "cum": "31580.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1800000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "4800000", + "notionalFloor": "1800000", + "maintMarginRatio": "0.1", + "cum": "121580.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 4800000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "4800000", + "maintMarginRatio": "0.125", + "cum": "241580.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "991580.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "18000000", + "maintMarginRatio": "0.5", + "cum": "5491580.0" + } + } + ], + "MINA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795700.0" + } + } + ], + "MKR/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 450000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "450000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "395.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 450000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "900000", + "notionalFloor": "450000", + "maintMarginRatio": "0.05", + "cum": "11645.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 900000.0, + "maxNotional": 2400000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2400000", + "notionalFloor": "900000", + "maintMarginRatio": "0.1", + "cum": "56645.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2400000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "3000000", + "notionalFloor": "2400000", + "maintMarginRatio": "0.125", + "cum": "116645.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 9000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "9000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.25", + "cum": "491645.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 9000000.0, + "maxNotional": 15000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "15000000", + "notionalFloor": "9000000", + "maintMarginRatio": "0.5", + "cum": "2741645.0" + } + } + ], + "MTL/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "NEAR/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "NEAR/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -15740,13 +17970,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 150000.0, - "maxNotional": 250000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "250000", + "notionalCap": "600000", "notionalFloor": "150000", "maintMarginRatio": "0.05", "cum": "4500.0" @@ -15755,7 +17985,349 @@ { "tier": 4.0, "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "34500.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "74500.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "324500.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1824500.0" + } + } + ], + "NEO/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3725770.0" + } + } + ], + "NKN/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "OCEAN/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, @@ -15763,9 +18335,775 @@ "bracket": "4", "initialLeverage": "5", "notionalCap": "500000", - "notionalFloor": "250000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "17000.0" + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "398150.0" + } + } + ], + "OGN/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "OMG/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "900000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 900000.0, + "maxNotional": 2400000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "2400000", + "notionalFloor": "900000", + "maintMarginRatio": "0.1", + "cum": "45650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 2400000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "3000000", + "notionalFloor": "2400000", + "maintMarginRatio": "0.125", + "cum": "105650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 9000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "9000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.25", + "cum": "480650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 9000000.0, + "maxNotional": 15000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "15000000", + "notionalFloor": "9000000", + "maintMarginRatio": "0.5", + "cum": "2730650.0" + } + } + ], + "ONE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "ONT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820700.0" + } + } + ], + "OP/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3725770.0" + } + } + ], + "PEOPLE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "300000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "800000", + "notionalFloor": "300000", + "maintMarginRatio": "0.1", + "cum": "15650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.125", + "cum": "35650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "910650.0" + } + } + ], + "PERP/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" } }, { @@ -15781,29 +19119,29 @@ "notionalCap": "1000000", "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "29500.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "2000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "154500.0" + "cum": "148150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, @@ -15811,13 +19149,1839 @@ "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "654500.0" + "cum": "898150.0" } } ], - "SC/USDT": [ + "PHB/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "PHB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "QNT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "QTUM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23200.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148200.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898200.0" + } + } + ], + "RAD/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "RAY/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "RDNT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "REEF/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "REN/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "RLC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "RNDR/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "ROSE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23200.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148200.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898200.0" + } + } + ], + "RSR/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "300000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "800000", + "notionalFloor": "300000", + "maintMarginRatio": "0.1", + "cum": "15650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.125", + "cum": "35650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "910650.0" + } + } + ], + "RUNE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "RVN/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386950.0" + } + } + ], + "SAND/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "SAND/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.007, + "maxLeverage": 40.0, + "info": { + "bracket": "2", + "initialLeverage": "40", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.007", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "80.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "830.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10830.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50830.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100830.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725830.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3725830.0" + } + } + ], + "SC/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -15915,394 +21079,18 @@ } } ], - "SFP/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 15000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "15000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 15000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "100000", - "notionalFloor": "15000", - "maintMarginRatio": "0.05", - "cum": "375.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5375.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11625.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386625.0" - } - } - ], - "SKL/USDT": [ + "SFP/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "SNX/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "SOL/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 11.0, - "info": { - "bracket": "1", - "initialLeverage": "11", - "notionalCap": "100000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "500000", - "notionalFloor": "100000", - "maintMarginRatio": "0.05", - "cum": "2500.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.1", - "cum": "27500.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "4", - "initialLeverage": "3", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.15", - "cum": "77500.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.25", - "cum": "277500.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 5000000.0, - "maxNotional": 8000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.5", - "cum": "1527500.0" - } - } - ], - "SOL/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 150000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 12.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "12", - "notionalCap": "150000", + "initialLeverage": "25", + "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", "cum": "0.0" @@ -16311,114 +21099,98 @@ { "tier": 2.0, "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "minNotional": 5000.0, + "maxNotional": 15000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 11.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "11", - "notionalCap": "250000", - "notionalFloor": "150000", + "initialLeverage": "20", + "notionalCap": "15000", + "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "750.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 15000.0, + "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalCap": "100000", + "notionalFloor": "15000", "maintMarginRatio": "0.05", - "cum": "7000.0" + "cum": "400.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 100000.0, + "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "250000", + "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "57000.0" + "cum": "5400.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 250000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maxLeverage": 2.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "107000.0" + "cum": "11650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "10000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.25", - "cum": "732000.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 11000000.0, + "minNotional": 3000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "6", "initialLeverage": "1", - "notionalCap": "11000000", - "notionalFloor": "10000000", + "notionalCap": "8000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "3232000.0" + "cum": "1136650.0" } } ], - "SPELL/USDT": [ + "SKL/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -16435,7 +21207,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -16451,7 +21223,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -16467,7 +21239,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -16483,27 +21255,483 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "SRM/USDT": [ + "SNX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45700.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795700.0" + } + } + ], + "SOL/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 50000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "100000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "250.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "500000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2750.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "27750.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "5", + "initialLeverage": "3", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.15", + "cum": "77750.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "277750.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 5000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.5", + "cum": "1527750.0" + } + } + ], + "SOL/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "50000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "150000", + "notionalFloor": "50000", + "maintMarginRatio": "0.02", + "cum": "500.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 150000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "900000", + "notionalFloor": "150000", + "maintMarginRatio": "0.025", + "cum": "1250.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 900000.0, + "maxNotional": 1800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1800000", + "notionalFloor": "900000", + "maintMarginRatio": "0.05", + "cum": "23750.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1800000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "4800000", + "notionalFloor": "1800000", + "maintMarginRatio": "0.1", + "cum": "113750.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 4800000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "4800000", + "maintMarginRatio": "0.125", + "cum": "233750.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "983750.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "18000000", + "maintMarginRatio": "0.5", + "cum": "5483750.0" + } + } + ], + "SPELL/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "SRM/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -16585,20 +21813,20 @@ } } ], - "STG/USDT": [ + "SSV/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -16608,160 +21836,62 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "STMX/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23150.0" } }, { @@ -16769,19 +21899,247 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, "info": { "bracket": "6", - "initialLeverage": "1", + "initialLeverage": "2", "notionalCap": "3000000", "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "898150.0" } } ], - "STORJ/USDT": [ + "STG/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "STMX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 10.0, + "info": { + "bracket": "1", + "initialLeverage": "10", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 8.0, + "info": { + "bracket": "2", + "initialLeverage": "8", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "STORJ/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -16818,13 +22176,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -16833,53 +22191,313 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "15700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "35700.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "910700.0" } } ], - "SUSHI/USDT": [ + "STX/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "50000", + "maintMarginRatio": "0.05", + "cum": "1325.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "31325.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "71325.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "321325.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1821325.0" + } + } + ], + "SUI/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "300000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "300.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 300000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "600000", + "notionalFloor": "300000", + "maintMarginRatio": "0.05", + "cum": "7800.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "37800.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "77800.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "327800.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1827800.0" + } + } + ], + "SUSHI/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -16916,13 +22534,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -16931,66 +22549,82 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820700.0" } } ], - "SXP/USDT": [ + "SXP/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 15.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "15", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -17000,14 +22634,128 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 10.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "600000", + "maintMarginRatio": "0.1", + "cum": "30650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "70650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 6500000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "6500000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1820650.0" + } + } + ], + "T/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { @@ -17023,7 +22771,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -17039,7 +22787,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -17055,7 +22803,7 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { @@ -17071,11 +22819,11 @@ "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "THETA/USDT": [ + "THETA/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -17205,102 +22953,232 @@ } } ], - "TLM/BUSD": [ + "TLM/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "8", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" } }, { "tier": 3.0, "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" + "cum": "5650.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "BUSD", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "11900.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "386900.0" } } ], - "TLM/USDT": [ + "TLM/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "TOMO/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "1", "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -17317,88 +23195,104 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 480000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "480000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 480000.0, + "maxNotional": 1280000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1280000", + "notionalFloor": "480000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "24650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1280000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "1600000", + "notionalFloor": "1280000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "56650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 1600000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "4800000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.25", + "cum": "256650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 4800000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4800000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1456650.0" } } ], - "TOMO/USDT": [ + "TRB/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -17415,7 +23309,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -17431,7 +23325,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -17447,7 +23341,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -17463,40 +23357,40 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "386900.0" } } ], - "TRB/USDT": [ + "TRU/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -17506,164 +23400,212 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "898150.0" } } ], - "TRX/BUSD": [ + "TRX/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "0.0" } }, { "tier": 2.0, "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "2", + "bracket": "3", "initialLeverage": "10", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "625.0" + "cum": "650.0" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "BUSD", "minNotional": 100000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "5", "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" + "cum": "5650.0" } }, { "tier": 5.0, "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 250000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.25", + "cum": "199400.0" + } + }, + { + "tier": 7.0, + "currency": "BUSD", + "minNotional": 3000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "949400.0" } } ], - "TRX/USDT": [ + "TRX/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -17684,13 +23626,13 @@ "tier": 2.0, "currency": "USDT", "minNotional": 10000.0, - "maxNotional": 50000.0, + "maxNotional": 90000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 25.0, "info": { "bracket": "2", "initialLeverage": "25", - "notionalCap": "50000", + "notionalCap": "90000", "notionalFloor": "10000", "maintMarginRatio": "0.01", "cum": "35.0" @@ -17699,87 +23641,87 @@ { "tier": 3.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 250000.0, + "minNotional": 90000.0, + "maxNotional": 645000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "250000", - "notionalFloor": "50000", + "notionalCap": "645000", + "notionalFloor": "90000", "maintMarginRatio": "0.02", - "cum": "535.0" + "cum": "935.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 645000.0, + "maxNotional": 1200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalCap": "1200000", + "notionalFloor": "645000", "maintMarginRatio": "0.05", - "cum": "8035.0" + "cum": "20285.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 1200000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "3000000", + "notionalFloor": "1200000", "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "80285.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 3000000.0, + "maxNotional": 6000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "6000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.125", - "cum": "108035.0" + "cum": "155285.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 6000000.0, + "maxNotional": 12000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "7", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "12000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.15", - "cum": "233035.0" + "cum": "305285.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 10000000.0, + "minNotional": 12000000.0, "maxNotional": 20000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, @@ -17787,9 +23729,9 @@ "bracket": "8", "initialLeverage": "2", "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.25", - "cum": "1233035.0" + "cum": "1505285.0" } }, { @@ -17805,24 +23747,24 @@ "notionalCap": "30000000", "notionalFloor": "20000000", "maintMarginRatio": "0.5", - "cum": "6233035.0" + "cum": "6505285.0" } } ], - "UNFI/USDT": [ + "UMA/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -17832,553 +23774,111 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "UNI/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "4", + "bracket": "6", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" } }, { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "5", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386875.0" + "cum": "898150.0" } } ], - "UNI/USDT": [ + "UNFI/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "VET/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "WAVES/BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "WAVES/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, - "info": { - "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "WOO/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, + "maintenanceMarginRate": 0.02, "maxLeverage": 20.0, "info": { "bracket": "1", "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -18395,7 +23895,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -18411,7 +23911,7 @@ "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -18427,7 +23927,7 @@ "notionalCap": "250000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "5650.0" } }, { @@ -18443,105 +23943,7 @@ "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", - "cum": "11950.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "XEM/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "11900.0" } }, { @@ -18557,11 +23959,923 @@ "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "UNI/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 8.0, + "info": { + "bracket": "1", + "initialLeverage": "8", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 7.0, + "info": { + "bracket": "2", + "initialLeverage": "7", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "UNI/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "900000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 900000.0, + "maxNotional": 1800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1800000", + "notionalFloor": "900000", + "maintMarginRatio": "0.05", + "cum": "23270.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1800000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "4800000", + "notionalFloor": "1800000", + "maintMarginRatio": "0.1", + "cum": "113270.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 4800000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "4800000", + "maintMarginRatio": "0.125", + "cum": "233270.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "983270.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "18000000", + "maintMarginRatio": "0.5", + "cum": "5483270.0" + } + } + ], + "USDC/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 30.0, + "info": { + "bracket": "1", + "initialLeverage": "30", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "600000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 600000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "600000", + "maintMarginRatio": "0.05", + "cum": "15770.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1200000.0, + "maxNotional": 3200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "3200000", + "notionalFloor": "1200000", + "maintMarginRatio": "0.1", + "cum": "75770.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3200000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "3200000", + "maintMarginRatio": "0.125", + "cum": "155770.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "780770.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3780770.0" + } + } + ], + "VET/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "395.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "200000", + "maintMarginRatio": "0.05", + "cum": "5395.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "25395.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "50395.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "675395.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "2175395.0" + } + } + ], + "WAVES/BUSD:BUSD": [ + { + "tier": 1.0, + "currency": "BUSD", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 10.0, + "info": { + "bracket": "1", + "initialLeverage": "10", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "BUSD", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 8.0, + "info": { + "bracket": "2", + "initialLeverage": "8", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "BUSD", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 6.0, + "info": { + "bracket": "3", + "initialLeverage": "6", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "BUSD", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "BUSD", + "minNotional": 250000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "500000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "BUSD", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.5", + "cum": "199400.0" + } + } + ], + "WAVES/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "WOO/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.01", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "700.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5700.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11950.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", "cum": "386950.0" } } ], - "XLM/USDT": [ + "XEM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "1", + "initialLeverage": "25", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "400000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 400000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "400000", + "maintMarginRatio": "0.1", + "cum": "20650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "45650.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.5", + "cum": "1795650.0" + } + } + ], + "XLM/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -18675,7 +24989,7 @@ } } ], - "XMR/USDT": [ + "XMR/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -18789,7 +25103,7 @@ } } ], - "XRP/BUSD": [ + "XRP/BUSD:BUSD": [ { "tier": 1.0, "currency": "BUSD", @@ -18887,101 +25201,101 @@ } } ], - "XRP/USDT": [ + "XRP/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 50.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", - "notionalCap": "10000", + "initialLeverage": "75", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", "minNotional": 10000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 40.0, "info": { - "bracket": "2", + "bracket": "3", "initialLeverage": "40", "notionalCap": "50000", "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "35.0" + "cum": "45.0" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "25", "notionalCap": "250000", "notionalFloor": "50000", "maintMarginRatio": "0.02", - "cum": "535.0" + "cum": "545.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "10", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.05", - "cum": "8035.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "5", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "8045.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 1000000.0, "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "4", + "initialLeverage": "5", "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "108035.0" + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "58045.0" } }, { @@ -18989,15 +25303,15 @@ "currency": "USDT", "minNotional": 5000000.0, "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "7", - "initialLeverage": "3", + "initialLeverage": "4", "notionalCap": "10000000", "notionalFloor": "5000000", - "maintMarginRatio": "0.15", - "cum": "233035.0" + "maintMarginRatio": "0.125", + "cum": "183045.0" } }, { @@ -19005,35 +25319,51 @@ "currency": "USDT", "minNotional": 10000000.0, "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, "info": { "bracket": "8", - "initialLeverage": "2", + "initialLeverage": "3", "notionalCap": "20000000", "notionalFloor": "10000000", - "maintMarginRatio": "0.25", - "cum": "1233035.0" + "maintMarginRatio": "0.15", + "cum": "433045.0" } }, { "tier": 9.0, "currency": "USDT", "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2433045.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "9", + "bracket": "10", "initialLeverage": "1", "notionalCap": "50000000", - "notionalFloor": "20000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "6233035.0" + "cum": "9933045.0" } } ], - "XTZ/USDT": [ + "XTZ/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -19147,7 +25477,121 @@ } } ], - "YFI/USDT": [ + "XVS/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "YFI/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -19184,13 +25628,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "400000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -19199,53 +25643,69 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "20700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "45700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "295700.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1795700.0" } } ], - "ZEC/USDT": [ + "ZEC/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -19359,7 +25819,105 @@ } } ], - "ZEN/USDT": [ + "ZEN/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11900.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386900.0" + } + } + ], + "ZIL/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -19396,13 +25954,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "700.0" @@ -19411,151 +25969,69 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30700.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70700.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386950.0" - } - } - ], - "ZIL/USDT": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, - "info": { - "bracket": "1", - "initialLeverage": "20", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.01", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "75.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "700.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5700.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11950.0" + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320700.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 6000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820700.0" } } ], - "ZRX/USDT": [ + "ZRX/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", diff --git a/freqtrade/exchange/bitvavo.py b/freqtrade/exchange/bitvavo.py new file mode 100644 index 000000000..ba1d355cc --- /dev/null +++ b/freqtrade/exchange/bitvavo.py @@ -0,0 +1,23 @@ +"""Kucoin exchange subclass.""" +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bitvavo(Exchange): + """Bitvavo exchange class. + + Contains adjustments needed for Freqtrade to work with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 1440, + } diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index d14c7c192..a4b070741 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,9 +1,16 @@ """ Bybit exchange subclass """ import logging -from typing import Dict, List, Tuple +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple -from freqtrade.enums import MarginMode, TradingMode +import ccxt + +from freqtrade.constants import BuySell +from freqtrade.enums import MarginMode, PriceType, TradingMode +from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier +from freqtrade.exchange.exchange_utils import timeframe_to_msecs logger = logging.getLogger(__name__) @@ -20,18 +27,27 @@ class Bybit(Exchange): """ _ft_has: Dict = { - "ohlcv_candle_limit": 1000, - "ccxt_futures_name": "linear", + "ohlcv_candle_limit": 200, "ohlcv_has_history": False, } _ft_has_futures: Dict = { "ohlcv_has_history": True, + "mark_ohlcv_timeframe": "4h", + "funding_fee_timeframe": "8h", + "stoploss_on_exchange": True, + "stoploss_order_types": {"limit": "limit", "market": "market"}, + "stop_price_type_field": "triggerBy", + "stop_price_type_value_mapping": { + PriceType.LAST: "LastPrice", + PriceType.MARK: "MarkPrice", + PriceType.INDEX: "IndexPrice", + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ # TradingMode.SPOT always supported and not required in this list # (TradingMode.FUTURES, MarginMode.CROSS), - # (TradingMode.FUTURES, MarginMode.ISOLATED) + (TradingMode.FUTURES, MarginMode.ISOLATED) ] @property @@ -47,3 +63,158 @@ class Bybit(Exchange): }) config.update(super()._ccxt_config) return config + + def market_is_future(self, market: Dict[str, Any]) -> bool: + main = super().market_is_future(market) + # For ByBit, we'll only support USDT markets for now. + return ( + main and market['settle'] == 'USDT' + ) + + @retrier + def additional_exchange_init(self) -> None: + """ + Additional exchange initialization logic. + .api will be available at this point. + Must be overridden in child methods if required. + """ + try: + if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: + position_mode = self._api.set_position_mode(False) + self._log_exchange_response('set_position_mode', position_mode) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' + ) from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + async def _fetch_funding_rate_history( + self, + pair: str, + timeframe: str, + limit: int, + since_ms: Optional[int] = None, + ) -> List[List]: + """ + Fetch funding rate history + Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed. + """ + params = {} + if since_ms: + until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit']) + params.update({'until': until}) + # Funding rate + data = await self._api_async.fetch_funding_rate_history( + pair, since=since_ms, + params=params) + # Convert funding rate to candle pattern + 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, 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) + self._set_leverage(leverage, pair, accept_fail=True) + + def _get_params( + self, + side: BuySell, + ordertype: str, + leverage: float, + reduceOnly: bool, + time_in_force: str = 'GTC', + ) -> Dict: + params = super()._get_params( + side=side, + ordertype=ordertype, + leverage=leverage, + reduceOnly=reduceOnly, + time_in_force=time_in_force, + ) + if self.trading_mode == TradingMode.FUTURES and self.margin_mode: + params['position_idx'] = 0 + return params + + def dry_run_liquidation_price( + self, + pair: str, + open_rate: float, # Entry price of position + is_short: bool, + amount: float, + stake_amount: float, + leverage: float, + wallet_balance: float, # Or margin balance + mm_ex_1: float = 0.0, # (Binance) Cross only + upnl_ex_1: float = 0.0, # (Binance) Cross only + ) -> Optional[float]: + """ + Important: Must be fetching data from cached values as this is used by backtesting! + PERPETUAL: + bybit: + https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067 + + Long: + Liquidation Price = ( + Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate) + - Extra Margin Added/ Contract) + Short: + Liquidation Price = ( + Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate) + + Extra Margin Added/ Contract) + + Implementation Note: Extra margin is currently not used. + + :param pair: Pair to calculate liquidation price for + :param open_rate: Entry price of position + :param is_short: True if the trade is a short, false otherwise + :param amount: Absolute value of position size incl. leverage (in base currency) + :param stake_amount: Stake amount - Collateral in settle currency. + :param leverage: Leverage used for this position. + :param trading_mode: SPOT, MARGIN, FUTURES, etc. + :param margin_mode: Either ISOLATED or CROSS + :param wallet_balance: Amount of margin_mode in the wallet being used to trade + Cross-Margin Mode: crossWalletBalance + Isolated-Margin Mode: isolatedWalletBalance + """ + + market = self.markets[pair] + mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount) + + if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED: + + if market['inverse']: + raise OperationalException( + "Freqtrade does not yet support inverse contracts") + initial_margin_rate = 1 / leverage + + # See docstring - ignores extra margin! + if is_short: + return open_rate * (1 + initial_margin_rate - mm_ratio) + else: + return open_rate * (1 - initial_margin_rate + mm_ratio) + + else: + raise OperationalException( + "Freqtrade only supports isolated futures for leverage trading") + + def get_funding_fees( + self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float: + """ + Fetch funding fees, either from the exchange (live) or calculates them + based on funding rate/mark price history + :param pair: The quote/base pair of the trade + :param is_short: trade direction + :param amount: Trade amount + :param open_date: Open date of the trade + :return: funding fee since open_date + :raises: ExchangeError if something goes wrong. + """ + # Bybit does not provide "applied" funding fees per position. + if self.trading_mode == TradingMode.FUTURES: + return self._fetch_and_calculate_funding_fees( + pair, amount, is_short, open_date) + return 0.0 diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 6d09c4f95..10dfdf178 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -4,6 +4,7 @@ import time from functools import wraps from typing import Any, Callable, Optional, TypeVar, cast, overload +from freqtrade.constants import ExchangeConfig from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError from freqtrade.mixins import LoggingMixin @@ -46,13 +47,13 @@ MAP_EXCHANGE_CHILDCLASS = { 'binanceje': 'binance', 'binanceusdm': 'binance', 'okex': 'okx', - 'gate': 'gateio', + 'gateio': 'gate', } SUPPORTED_EXCHANGES = [ 'binance', 'bittrex', - 'gateio', + 'gate', 'huobi', 'kraken', 'okx', @@ -84,20 +85,22 @@ EXCHANGE_HAS_OPTIONAL = [ # 'fetchPositions', # Futures trading # 'fetchLeverageTiers', # Futures initialization # 'fetchMarketLeverageTiers', # Futures initialization + # 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance... ] -def remove_credentials(config) -> None: +def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) -> None: """ Removes exchange keys from the configuration and specifies dry-run Used for backtesting / hyperopt / edge and utils. Modifies the input dict! """ - if config.get('dry_run', False): - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - config['exchange']['password'] = '' - config['exchange']['uid'] = '' + if dry_run: + exchange_config['key'] = '' + exchange_config['apiKey'] = '' + exchange_config['secret'] = '' + exchange_config['password'] = '' + exchange_config['uid'] = '' def calculate_backoff(retrycount, max_retries): diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c72f9479d..3b1466c69 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -3,15 +3,14 @@ Cryptocurrency Exchanges support """ import asyncio -import http import inspect import logging from copy import deepcopy from datetime import datetime, timedelta, timezone +from math import floor from threading import Lock from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union -import arrow import ccxt import ccxt.async_support as ccxt_async from cachetools import TTLCache @@ -20,37 +19,35 @@ from dateutil import parser from pandas import DataFrame, concat from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk, - BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker, - PairWithTimeframe) + BuySell, Config, EntryExit, ExchangeConfig, + ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe) from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode +from freqtrade.enums.pricetype import PriceType from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier, - retrier_async) -from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision, - amount_to_contracts, amount_to_precision, - contracts_to_amount, date_minus_candles, - is_exchange_known_ccxt, market_is_active, - price_to_precision, timeframe_to_minutes, - timeframe_to_msecs, timeframe_to_next_date, - timeframe_to_prev_date, timeframe_to_seconds) -from freqtrade.exchange.types import OHLCVResponse, Ticker, Tickers +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_exchange_credentials, + retrier, retrier_async) +from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType, + amount_to_contract_precision, amount_to_contracts, + amount_to_precision, contracts_to_amount, + date_minus_candles, is_exchange_known_ccxt, + market_is_active, price_to_precision, + timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, timeframe_to_prev_date, + timeframe_to_seconds) +from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json, safe_value_fallback2) from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist +from freqtrade.util import dt_from_ts, dt_now +from freqtrade.util.datetime_helpers import dt_humanize, dt_ts logger = logging.getLogger(__name__) -# Workaround for adding samesite support to pre 3.8 python -# Only applies to python3.7, and only on certain exchanges (kraken) -# Replicates the fix from starlette (which is actually causing this problem) -http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore - - class Exchange: # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) @@ -64,8 +61,8 @@ class Exchange: # or by specifying them in the configuration. _ft_has_default: Dict = { "stoploss_on_exchange": False, + "stop_price_param": "stopPrice", "order_time_in_force": ["GTC"], - "time_in_force_parameter": "timeInForce", "ohlcv_params": {}, "ohlcv_candle_limit": 500, "ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv @@ -74,6 +71,7 @@ class Exchange: # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency "ohlcv_volume_currency": "base", # "base" or "quote" "tickers_have_quoteVolume": True, + "tickers_have_bid_ask": True, # bid / ask empty for fetch_tickers "tickers_have_price": True, "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", @@ -85,6 +83,8 @@ class Exchange: "fee_cost_in_contracts": False, # Fee cost needs contract conversion "needs_trading_fees": False, # use fetch_trading_fees to cache fees "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'], + # Override createMarketBuyOrderRequiresPrice where ccxt has it wrong + "marketOrderRequiresPrice": False, } _ft_has: Dict = {} _ft_has_futures: Dict = {} @@ -93,8 +93,8 @@ class Exchange: # TradingMode.SPOT always supported and not required in this list ] - def __init__(self, config: Config, validate: bool = True, - load_leverage_tiers: bool = False) -> None: + def __init__(self, config: Config, *, exchange_config: Optional[ExchangeConfig] = None, + validate: bool = True, load_leverage_tiers: bool = False) -> None: """ Initializes this module with the given config, it does basic validation whether the specified exchange and pairs are valid. @@ -108,8 +108,7 @@ class Exchange: # Lock event loop. This is necessary to avoid race-conditions when using force* commands # Due to funding fee fetching. self._loop_lock = Lock() - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) + self.loop = self._init_async_loop() self._config: Config = {} self._config.update(config) @@ -133,13 +132,13 @@ class Exchange: # Holds all open sell orders for dry_run self._dry_run_open_orders: Dict[str, Any] = {} - remove_credentials(config) if config['dry_run']: logger.info('Instance is running with dry_run enabled') logger.info(f"Using CCXT {ccxt.__version__}") - exchange_config = config['exchange'] - self.log_responses = exchange_config.get('log_responses', False) + exchange_conf: Dict[str, Any] = exchange_config if exchange_config else config['exchange'] + remove_exchange_credentials(exchange_conf, config.get('dry_run', False)) + self.log_responses = exchange_conf.get('log_responses', False) # Leverage properties self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) @@ -154,8 +153,8 @@ class Exchange: self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default)) if self.trading_mode == TradingMode.FUTURES: self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has) - if exchange_config.get('_ft_has_params'): - self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'), + if exchange_conf.get('_ft_has_params'): + self._ft_has = deep_merge_dicts(exchange_conf.get('_ft_has_params'), self._ft_has) logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has) @@ -167,18 +166,18 @@ class Exchange: # Initialize ccxt objects ccxt_config = self._ccxt_config - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) + ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}), ccxt_config) + ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_sync_config', {}), ccxt_config) - self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) + self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config) ccxt_async_config = self._ccxt_config - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), + ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}), ccxt_async_config) - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), + ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_async_config', {}), ccxt_async_config) self._api_async = self._init_ccxt( - exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config) logger.info(f'Using Exchange "{self.name}"') self.required_candle_call_count = 1 @@ -191,7 +190,7 @@ class Exchange: self._startup_candle_count, config.get('timeframe', '')) # Converts the interval provided in minutes in config to seconds - self.markets_refresh_interval: int = exchange_config.get( + self.markets_refresh_interval: int = exchange_conf.get( "markets_refresh_interval", 60) * 60 if self.trading_mode != TradingMode.SPOT and load_leverage_tiers: @@ -210,6 +209,13 @@ class Exchange: and self._api_async.session): logger.debug("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) + if self.loop and not self.loop.is_closed(): + self.loop.close() + + def _init_async_loop(self) -> asyncio.AbstractEventLoop: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop def validate_config(self, config): # Check if timeframe is available @@ -485,7 +491,7 @@ class Exchange: try: self._markets = self._api.load_markets(params={}) self._load_async_markets() - self._last_markets_refresh = arrow.utcnow().int_timestamp + self._last_markets_refresh = dt_ts() if self._ft_has['needs_trading_fees']: self._trading_fees = self.fetch_trading_fees() @@ -496,15 +502,14 @@ class Exchange: """Reload markets both sync and async if refresh interval has passed """ # Check whether markets have to be reloaded if (self._last_markets_refresh > 0) and ( - self._last_markets_refresh + self.markets_refresh_interval - > arrow.utcnow().int_timestamp): + self._last_markets_refresh + self.markets_refresh_interval > dt_ts()): return None logger.debug("Performing scheduled market reload..") try: self._markets = self._api.load_markets(reload=True, params={}) # Also reload async markets to avoid issues with newly listed pairs self._load_async_markets(reload=True) - self._last_markets_refresh = arrow.utcnow().int_timestamp + self._last_markets_refresh = dt_ts() self.fill_leverage_tiers() except ccxt.BaseError: logger.exception("Could not reload markets.") @@ -606,12 +611,27 @@ class Exchange: if not self.exchange_has('createMarketOrder'): raise OperationalException( f'Exchange {self.name} does not support market orders.') + self.validate_stop_ordertypes(order_types) + def validate_stop_ordertypes(self, order_types: Dict) -> None: + """ + Validate stoploss order types + """ if (order_types.get("stoploss_on_exchange") and not self._ft_has.get("stoploss_on_exchange", False)): raise OperationalException( f'On exchange stoploss is not supported for {self.name}.' ) + if self.trading_mode == TradingMode.FUTURES: + price_mapping = self._ft_has.get('stop_price_type_value_mapping', {}).keys() + if ( + order_types.get("stoploss_on_exchange", False) is True + and 'stoploss_price_type' in order_types + and order_types['stoploss_price_type'] not in price_mapping + ): + raise OperationalException( + f'On exchange stoploss price type is not supported for {self.name}.' + ) def validate_pricing(self, pricing: Dict) -> None: if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'): @@ -682,7 +702,7 @@ class Exchange: f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}" ) - def get_option(self, param: str, default: Any = None) -> Any: + def get_option(self, param: str, default: Optional[Any] = None) -> Any: """ Get parameter value from _ft_has """ @@ -720,12 +740,14 @@ class Exchange: """ return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode) - def price_to_precision(self, pair: str, price: float) -> float: + def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float: """ - Returns the price rounded up to the precision the Exchange accepts. - Rounds up + Returns the price rounded to the precision the Exchange accepts. + The default price_rounding_mode in conf is ROUND. + For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts. """ - return price_to_precision(price, self.get_precision_price(pair), self.precisionMode) + return price_to_precision(price, self.get_precision_price(pair), + self.precisionMode, rounding_mode=rounding_mode) def price_get_one_pip(self, pair: str, price: float) -> float: """ @@ -748,12 +770,12 @@ class Exchange: return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage) def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float: - max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max') + max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage) if max_stake_amount is None: # * Should never be executed raise OperationalException(f'{self.name}.get_max_pair_stake_amount should' 'never set max_stake_amount to None') - return max_stake_amount / leverage + return max_stake_amount def _get_stake_amount_limit( self, @@ -771,43 +793,41 @@ class Exchange: except KeyError: raise ValueError(f"Can't get market information for symbol {pair}") + if isMin: + # reserve some percent defined in config (5% default) + stoploss + margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent', + DEFAULT_AMOUNT_RESERVE_PERCENT) + stoploss_reserve = ( + margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + ) + # it should not be more than 50% + stoploss_reserve = max(min(stoploss_reserve, 1.5), 1) + else: + margin_reserve = 1.0 + stoploss_reserve = 1.0 + stake_limits = [] limits = market['limits'] if (limits['cost'][limit] is not None): stake_limits.append( - self._contracts_to_amount( - pair, - limits['cost'][limit] - ) + self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve ) if (limits['amount'][limit] is not None): stake_limits.append( - self._contracts_to_amount( - pair, - limits['amount'][limit] * price - ) + self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve ) if not stake_limits: return None if isMin else float('inf') - # reserve some percent defined in config (5% default) + stoploss - amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', - DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent = ( - amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 - ) - # it should not be more than 50% - amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) - # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. return self._get_stake_amount_considering_leverage( - max(stake_limits) * amount_reserve_percent, + max(stake_limits) if isMin else min(stake_limits), leverage or 1.0 - ) if isMin else min(stake_limits) + ) def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float: """ @@ -823,7 +843,8 @@ class Exchange: def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, leverage: float, params: Dict = {}, stop_loss: bool = False) -> Dict[str, Any]: - order_id = f'dry_run_{side}_{datetime.now().timestamp()}' + now = dt_now() + order_id = f'dry_run_{side}_{now.timestamp()}' # Rounding here must respect to contract sizes _amount = self._contracts_to_amount( pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))) @@ -838,32 +859,45 @@ class Exchange: 'side': side, 'filled': 0, 'remaining': _amount, - 'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'), - 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'status': "closed" if ordertype == "market" and not stop_loss else "open", + 'datetime': now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), + 'timestamp': dt_ts(now), + 'status': "open", 'fee': None, 'info': {}, 'leverage': leverage } if stop_loss: dry_order["info"] = {"stopPrice": dry_order["price"]} - dry_order["stopPrice"] = dry_order["price"] + dry_order[self._ft_has['stop_price_param']] = dry_order["price"] # Workaround to avoid filling stoploss orders immediately dry_order["ft_order_type"] = "stoploss" + orderbook: Optional[OrderBook] = None + if self.exchange_has('fetchL2OrderBook'): + orderbook = self.fetch_l2_order_book(pair, 20) + if ordertype == "limit" and orderbook: + # Allow a 3% price difference + allowed_diff = 0.03 + if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff): + logger.info( + f"Converted order {pair} to market order due to price {rate} crossing spread " + f"by more than {allowed_diff:.2%}.") + dry_order["type"] = "market" if dry_order["type"] == "market" and not dry_order.get("ft_order_type"): # Update market order pricing - average = self.get_dry_market_fill_price(pair, side, amount, rate) + average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook) dry_order.update({ 'average': average, 'filled': _amount, 'remaining': 0.0, - 'cost': (dry_order['amount'] * average) / leverage + 'status': "closed", + 'cost': (dry_order['amount'] * average) }) # market orders will always incurr taker fees dry_order = self.add_dry_order_fee(pair, dry_order, 'taker') - dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True) + dry_order = self.check_dry_limit_order_filled( + dry_order, immediate=True, orderbook=orderbook) self._dry_run_open_orders[dry_order["id"]] = dry_order # Copy order and close it - so the returned order is open unless it's a market order @@ -885,20 +919,22 @@ class Exchange: }) return dry_order - def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float: + def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float, + orderbook: Optional[OrderBook]) -> float: """ Get the market order fill price based on orderbook interpolation """ if self.exchange_has('fetchL2OrderBook'): - ob = self.fetch_l2_order_book(pair, 20) - ob_type = 'asks' if side == 'buy' else 'bids' + if not orderbook: + orderbook = self.fetch_l2_order_book(pair, 20) + ob_type: OBLiteral = 'asks' if side == 'buy' else 'bids' slippage = 0.05 max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage)) remaining_amount = amount filled_amount = 0.0 book_entry_price = 0.0 - for book_entry in ob[ob_type]: + for book_entry in orderbook[ob_type]: book_entry_price = book_entry[0] book_entry_coin_volume = book_entry[1] if remaining_amount > 0: @@ -926,20 +962,20 @@ class Exchange: return rate - def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool: + def _dry_is_price_crossed(self, pair: str, side: str, limit: float, + orderbook: Optional[OrderBook] = None, offset: float = 0.0) -> bool: if not self.exchange_has('fetchL2OrderBook'): return True - ob = self.fetch_l2_order_book(pair, 1) + if not orderbook: + orderbook = self.fetch_l2_order_book(pair, 1) try: if side == 'buy': - price = ob['asks'][0][0] - logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}") - if limit >= price: + price = orderbook['asks'][0][0] + if limit * (1 - offset) >= price: return True else: - price = ob['bids'][0][0] - logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}") - if limit <= price: + price = orderbook['bids'][0][0] + if limit * (1 + offset) <= price: return True except IndexError: # Ignore empty orderbooks when filling - can be filled with the next iteration. @@ -947,7 +983,8 @@ class Exchange: return False def check_dry_limit_order_filled( - self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]: + self, order: Dict[str, Any], immediate: bool = False, + orderbook: Optional[OrderBook] = None) -> Dict[str, Any]: """ Check dry-run limit order fill and update fee (if it filled). """ @@ -955,7 +992,7 @@ class Exchange: and order['type'] in ["limit"] and not order.get('ft_order_type')): pair = order['symbol'] - if self._is_dry_limit_order_filled(pair, order['side'], order['price']): + if self._dry_is_price_crossed(pair, order['side'], order['price'], orderbook): order.update({ 'status': 'closed', 'filled': order['amount'], @@ -983,7 +1020,7 @@ class Exchange: from freqtrade.persistence import Order order = Order.order_by_id(order_id) if order: - ccxt_order = order.to_ccxt_object() + ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_param']) self._dry_run_open_orders[order_id] = ccxt_order return ccxt_order # Gracefully handle errors with dry-run orders. @@ -992,10 +1029,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, @@ -1007,12 +1044,18 @@ class Exchange: ) -> Dict: params = self._params.copy() if time_in_force != 'GTC' and ordertype != 'market': - param = self._ft_has.get('time_in_force_parameter', '') - params.update({param: time_in_force.upper()}) + params.update({'timeInForce': time_in_force.upper()}) if reduceOnly: params.update({'reduceOnly': True}) return params + def _order_needs_price(self, ordertype: str) -> bool: + return ( + ordertype != 'market' + or self._api.options.get("createMarketBuyOrderRequiresPrice", False) + or self._ft_has.get('marketOrderRequiresPrice', False) + ) + def create_order( self, *, @@ -1035,8 +1078,7 @@ class Exchange: try: # Set the precision for amount and price(rate) as accepted by the exchange amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) - needs_price = (ordertype != 'market' - or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) + needs_price = self._order_needs_price(ordertype) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None if not reduceOnly: @@ -1060,7 +1102,7 @@ class Exchange: f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: - raise ExchangeError( + raise InvalidOrderException( f'Could not create {ordertype} {side} order on market {pair}. ' f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e @@ -1079,11 +1121,11 @@ class Exchange: """ if not self._ft_has.get('stoploss_on_exchange'): raise OperationalException(f"stoploss is not implemented for {self.name}.") - + price_param = self._ft_has['stop_price_param'] return ( - order.get('stopPrice', None) is None - or ((side == "sell" and stop_loss > float(order['stopPrice'])) or - (side == "buy" and stop_loss < float(order['stopPrice']))) + order.get(price_param, None) is None + or ((side == "sell" and stop_loss > float(order[price_param])) or + (side == "buy" and stop_loss < float(order[price_param]))) ) def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]: @@ -1110,19 +1152,26 @@ class Exchange: "sell" else (stop_price >= limit_rate)) # Ensure rate is less than stop price if bad_stop_price: - raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') + # This can for example happen if the stop / liquidation price is set to 0 + # Which is possible if a market-order closes right away. + # The InvalidOrderException will bubble up to exit_positions, where it will be + # handled gracefully. + raise InvalidOrderException( + "In stoploss limit order, stop price should be more than limit price. " + f"Stop price: {stop_price}, Limit price: {limit_rate}, " + f"Limit Price pct: {limit_price_pct}" + ) return limit_rate 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({'stopPrice': stop_price}) + # Verify if stopPrice works for your exchange, else configure stop_price_param + params.update({self._ft_has['stop_price_param']: stop_price}) return params @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, - side: BuySell, leverage: float) -> Dict: + def create_stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, + side: BuySell, leverage: float) -> Dict: """ creates a stoploss order. requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market @@ -1143,12 +1192,12 @@ class Exchange: user_order_type = order_types.get('stoploss', 'market') ordertype, user_order_type = self._get_stop_order_type(user_order_type) - - stop_price_norm = self.price_to_precision(pair, stop_price) + round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP + stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode) limit_rate = None if user_order_type == 'limit': limit_rate = self._get_stop_limit_rate(stop_price, order_types, side) - limit_rate = self.price_to_precision(pair, limit_rate) + limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode) if self._config['dry_run']: dry_order = self.create_dry_run_order( @@ -1167,10 +1216,14 @@ class Exchange: stop_price=stop_price_norm) if self.trading_mode == TradingMode.FUTURES: params['reduceOnly'] = True + if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has: + price_type = self._ft_has['stop_price_type_value_mapping'][ + order_types.get('stoploss_price_type', PriceType.LAST)] + params[self._ft_has['stop_price_type_field']] = price_type 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) @@ -1357,7 +1410,7 @@ class Exchange: raise OperationalException(e) from e @retrier - def fetch_positions(self, pair: str = None) -> List[Dict]: + def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]: """ Fetch positions from the exchange. If no pair is given, all positions are returned. @@ -1380,6 +1433,47 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @retrier(retries=0) + def fetch_orders(self, pair: str, since: datetime) -> List[Dict]: + """ + Fetch all orders for a pair "since" + :param pair: Pair for the query + :param since: Starting time for the query + """ + if self._config['dry_run']: + return [] + + def fetch_orders_emulate() -> List[Dict]: + orders = [] + if self.exchange_has('fetchClosedOrders'): + orders = self._api.fetch_closed_orders(pair, since=since_ms) + if self.exchange_has('fetchOpenOrders'): + orders_open = self._api.fetch_open_orders(pair, since=since_ms) + orders.extend(orders_open) + return orders + + try: + since_ms = int((since.timestamp() - 10) * 1000) + if self.exchange_has('fetchOrders'): + try: + orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms) + except ccxt.NotSupported: + # Some exchanges don't support fetchOrders + # attempt to fetch open and closed orders separately + orders = fetch_orders_emulate() + else: + orders = fetch_orders_emulate() + self._log_exchange_response('fetch_orders', orders) + orders = [self._order_contracts_to_amount(o) for o in orders] + return orders + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not fetch positions due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + @retrier def fetch_trading_fees(self) -> Dict[str, Any]: """ @@ -1497,7 +1591,7 @@ class Exchange: return result @retrier - def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: + def fetch_l2_order_book(self, pair: str, limit: int = 100) -> OrderBook: """ Get L2 order book from exchange. Can be limited to a certain amount (if supported). @@ -1540,7 +1634,7 @@ class Exchange: def get_rate(self, pair: str, refresh: bool, side: EntryExit, is_short: bool, - order_book: Optional[dict] = None, ticker: Optional[Ticker] = None) -> float: + order_book: Optional[OrderBook] = None, ticker: Optional[Ticker] = None) -> float: """ Calculates bid/ask target bid rate - between current ask price and last price @@ -1578,7 +1672,8 @@ class Exchange: logger.debug('order_book %s', order_book) # top 1 = index 0 try: - rate = order_book[f"{price_side}s"][order_book_top - 1][0] + obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks' + rate = order_book[obside][order_book_top - 1][0] except (IndexError, KeyError) as e: logger.warning( f"{pair} - {name} Price at location {order_book_top} from orderbook " @@ -1801,7 +1896,7 @@ class Exchange: def get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, candle_type: CandleType, is_new_pair: bool = False, - until_ms: int = None) -> List: + until_ms: Optional[int] = None) -> List: """ Get candle history using asyncio and returns the list of candles. Handles all async work for this. @@ -1836,11 +1931,11 @@ class Exchange: logger.debug( "one_call: %s msecs (%s)", one_call, - arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True) + dt_humanize(dt_now() - timedelta(milliseconds=one_call), only_distance=True) ) input_coroutines = [self._async_get_candle_history( pair, timeframe, candle_type, since) for since in - range(since_ms, until_ms or (arrow.utcnow().int_timestamp * 1000), one_call)] + range(since_ms, until_ms or dt_ts(), one_call)] data: List = [] # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling @@ -1930,7 +2025,8 @@ class Exchange: cache: bool, drop_incomplete: bool) -> DataFrame: # keeping last candle time as last refreshed time of the pair if ticks and cache: - self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000 + idx = -2 if drop_incomplete and len(ticks) > 1 else -1 + self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] // 1000 # keeping parsed dataframe in cache ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=drop_incomplete) @@ -1984,9 +2080,9 @@ class Exchange: continue # Deconstruct tuple (has 5 elements) pair, timeframe, c_type, ticks, drop_hint = res - drop_incomplete = drop_hint if drop_incomplete is None else drop_incomplete + drop_incomplete_ = drop_hint if drop_incomplete is None else drop_incomplete ohlcv_df = self._process_ohlcv_df( - pair, timeframe, c_type, ticks, cache, drop_incomplete) + pair, timeframe, c_type, ticks, cache, drop_incomplete_) results_df[(pair, timeframe, c_type)] = ohlcv_df @@ -2003,7 +2099,9 @@ class Exchange: # Timeframe in seconds interval_in_sec = timeframe_to_seconds(timeframe) plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec - return plr < arrow.utcnow().int_timestamp + # current,active candle open date + now = int(timeframe_to_prev_date(timeframe).timestamp()) + return plr < now @retrier_async async def _async_get_candle_history( @@ -2020,7 +2118,7 @@ class Exchange: """ try: # Fetch OHLCV asynchronously - s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else '' + s = '(' + dt_from_ts(since_ms).isoformat() + ') ' if since_ms is not None else '' logger.debug( "Fetching pair %s, %s, interval %s, since %s %s...", pair, candle_type, timeframe, since_ms, s @@ -2110,7 +2208,7 @@ class Exchange: logger.debug( "Fetching trades for pair %s, since %s %s...", pair, since, - '(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else '' + '(' + dt_from_ts(since).isoformat() + ') ' if since is not None else '' ) trades = await self._api_async.fetch_trades(pair, since=since, limit=1000) trades = self._trades_contracts_to_amount(trades) @@ -2319,12 +2417,12 @@ class Exchange: # Must fetch the leverage tiers for each market separately # * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets markets = self.markets - symbols = [] - for symbol, market in markets.items(): + symbols = [ + symbol for symbol, market in markets.items() if (self.market_is_future(market) - and market['quote'] == self._config['stake_currency']): - symbols.append(symbol) + and market['quote'] == self._config['stake_currency']) + ] tiers: Dict[str, List[Dict]] = {} @@ -2344,25 +2442,26 @@ class Exchange: else: logger.info("Using cached leverage_tiers.") - async def gather_results(): + async def gather_results(input_coro): return await asyncio.gather(*input_coro, return_exceptions=True) for input_coro in chunks(coros, 100): with self._loop_lock: - results = self.loop.run_until_complete(gather_results()) + results = self.loop.run_until_complete(gather_results(input_coro)) - for symbol, res in results: - tiers[symbol] = res + for res in results: + if isinstance(res, Exception): + logger.warning(f"Leverage tier exception: {repr(res)}") + continue + symbol, tier = res + tiers[symbol] = tier if len(coros) > 0: self.cache_leverage_tiers(tiers, self._config['stake_currency']) logger.info(f"Done initializing {len(symbols)} markets.") return tiers - else: - return {} - else: - return {} + return {} def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None: @@ -2378,14 +2477,17 @@ class Exchange: def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]: filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json" if filename.is_file(): - tiers = file_load_json(filename) - updated = tiers.get('updated') - if updated: - updated_dt = parser.parse(updated) - if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4): - logger.info("Cached leverage tiers are outdated. Will update.") - return None - return tiers['data'] + try: + tiers = file_load_json(filename) + updated = tiers.get('updated') + if updated: + updated_dt = parser.parse(updated) + if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4): + logger.info("Cached leverage tiers are outdated. Will update.") + return None + return tiers['data'] + except Exception: + logger.exception("Error loading cached leverage tiers. Refreshing.") return None def fill_leverage_tiers(self) -> None: @@ -2491,7 +2593,7 @@ class Exchange: self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None + accept_fail: bool = False, ): """ Set's the leverage before making a trade, in order to not @@ -2500,12 +2602,18 @@ class Exchange: if self._config['dry_run'] or not self.exchange_has("setLeverage"): # Some exchanges only support one margin_mode type return - + if self._ft_has.get('floor_leverage', False) is True: + # Rounding for binance ... + leverage = floor(leverage) try: res = self._api.set_leverage(symbol=pair, leverage=leverage) self._log_exchange_response('set_leverage', res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from 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 except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e @@ -2527,7 +2635,8 @@ class Exchange: return open_date.minute > 0 or open_date.second > 0 @retrier - def set_margin_mode(self, pair: str, margin_mode: MarginMode, params: dict = {}): + def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False, + params: dict = {}): """ Set's the margin mode on the exchange to cross or isolated for a specific pair :param pair: base/quote currency pair (e.g. "ADA/USDT") @@ -2541,6 +2650,10 @@ class Exchange: self._log_exchange_response('set_margin_mode', res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e + except ccxt.BadRequest as e: + if not accept_fail: + raise TemporaryError( + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e @@ -2674,7 +2787,7 @@ class Exchange: :param amount: Trade amount :param open_date: Open date of the trade :return: funding fee since open_date - :raies: ExchangeError if something goes wrong. + :raises: ExchangeError if something goes wrong. """ if self.trading_mode == TradingMode.FUTURES: if self._config['dry_run']: @@ -2694,6 +2807,7 @@ class Exchange: is_short: bool, amount: float, # Absolute value of position size stake_amount: float, + leverage: float, wallet_balance: float, mm_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only @@ -2707,14 +2821,15 @@ class Exchange: raise OperationalException( f"{self.name} does not support {self.margin_mode} {self.trading_mode}") - isolated_liq = None + liquidation_price = None if self._config['dry_run'] or not self.exchange_has("fetchPositions"): - isolated_liq = self.dry_run_liquidation_price( + liquidation_price = self.dry_run_liquidation_price( pair=pair, open_rate=open_rate, is_short=is_short, amount=amount, + leverage=leverage, stake_amount=stake_amount, wallet_balance=wallet_balance, mm_ex_1=mm_ex_1, @@ -2724,16 +2839,16 @@ class Exchange: positions = self.fetch_positions(pair) if len(positions) > 0: pos = positions[0] - isolated_liq = pos['liquidationPrice'] + liquidation_price = pos['liquidationPrice'] - if isolated_liq: - buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer - isolated_liq = ( - isolated_liq - buffer_amount + if liquidation_price is not None: + buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer + liquidation_price_buffer = ( + liquidation_price - buffer_amount if is_short else - isolated_liq + buffer_amount + liquidation_price + buffer_amount ) - return isolated_liq + return max(liquidation_price_buffer, 0.0) else: return None @@ -2744,6 +2859,7 @@ class Exchange: is_short: bool, amount: float, stake_amount: float, + leverage: float, wallet_balance: float, # Or margin balance mm_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only @@ -2751,7 +2867,7 @@ class Exchange: """ Important: Must be fetching data from cached values as this is used by backtesting! PERPETUAL: - gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price + gate: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price > Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) / [ 1 ± (Maintenance Margin Ratio + Taker Rate)] Wherein, "+" or "-" depends on whether the contract goes long or short: @@ -2765,13 +2881,14 @@ class Exchange: :param is_short: True if the trade is a short, false otherwise :param amount: Absolute value of position size incl. leverage (in base currency) :param stake_amount: Stake amount - Collateral in settle currency. + :param leverage: Leverage used for this position. :param trading_mode: SPOT, MARGIN, FUTURES, etc. :param margin_mode: Either ISOLATED or CROSS :param wallet_balance: Amount of margin_mode in the wallet being used to trade Cross-Margin Mode: crossWalletBalance Isolated-Margin Mode: isolatedWalletBalance - # * Not required by Gateio or OKX + # * Not required by Gate or OKX :param mm_ex_1: :param upnl_ex_1: """ @@ -2825,8 +2942,8 @@ class Exchange: if nominal_value >= tier['minNotional']: return (tier['maintenanceMarginRate'], tier['maintAmt']) - raise OperationalException("nominal value can not be lower than 0") + raise ExchangeError("nominal value can not be lower than 0") # The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it # describes the min amt for a tier, and the lowest tier will always go down to 0 else: - raise OperationalException(f"Cannot get maintenance ratio using {self.name}") + raise ExchangeError(f"Cannot get maintenance ratio using {self.name}") diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index cb6333869..c6c2d5a24 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -2,31 +2,34 @@ Exchange support utils """ from datetime import datetime, timedelta, timezone -from math import ceil +from math import ceil, floor from typing import Any, Dict, List, Optional, Tuple import ccxt -from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision +from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE, + TRUNCATE, decimal_to_precision) from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED from freqtrade.util import FtPrecise +from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts CcxtModuleType = Any -def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: +def is_exchange_known_ccxt( + exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) -def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: +def ccxt_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]: """ Return the list of all exchanges known to ccxt """ return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges -def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: +def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[str]: """ Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list """ @@ -86,7 +89,7 @@ def timeframe_to_msecs(timeframe: str) -> int: return ccxt.Exchange.parse_timeframe(timeframe) * 1000 -def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: +def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> datetime: """ Use Timeframe and determine the candle start date for this date. Does not round when given a candle start date. @@ -97,12 +100,11 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: if not date: date = datetime.now(timezone.utc) - new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000, - ROUND_DOWN) // 1000 - return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) + new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000 + return dt_from_ts(new_timestamp) -def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: +def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> datetime: """ Use Timeframe and determine next candle. :param timeframe: timeframe in string format (e.g. "5m") @@ -111,9 +113,8 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: """ if not date: date = datetime.now(timezone.utc) - new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000, - ROUND_UP) // 1000 - return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) + new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000 + return dt_from_ts(new_timestamp) def date_minus_candles( @@ -218,35 +219,51 @@ def amount_to_contract_precision( return amount -def price_to_precision(price: float, price_precision: Optional[float], - precisionMode: Optional[int]) -> float: +def price_to_precision( + price: float, + price_precision: Optional[float], + precisionMode: Optional[int], + *, + rounding_mode: int = ROUND, +) -> float: """ - Returns the price rounded up to the precision the Exchange accepts. + Returns the price rounded to the precision the Exchange accepts. Partial Re-implementation of ccxt internal method decimal_to_precision(), - which does not support rounding up + which does not support rounding up. + For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts. + TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and align with amount_to_precision(). - !!! Rounds up :param price: price to convert :param price_precision: price precision to use. Used from markets[pair]['precision']['price'] :param precisionMode: precision mode to use. Should be used from precisionMode one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE + :param rounding_mode: rounding mode to use. Defaults to ROUND :return: price rounded up to the precision the Exchange accepts - """ if price_precision is not None and precisionMode is not None: - # price = float(decimal_to_precision(price, rounding_mode=ROUND, - # precision=price_precision, - # counting_mode=self.precisionMode, - # )) if precisionMode == TICK_SIZE: + if rounding_mode == ROUND: + ticks = price / price_precision + rounded_ticks = round(ticks) + return rounded_ticks * price_precision precision = FtPrecise(price_precision) price_str = FtPrecise(price) missing = price_str % precision if not missing == FtPrecise("0"): - price = round(float(str(price_str - missing + precision)), 14) - else: - symbol_prec = price_precision - big_price = price * pow(10, symbol_prec) - price = ceil(big_price) / pow(10, symbol_prec) + return round(float(str(price_str - missing + precision)), 14) + return price + elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES): + ndigits = round(price_precision) + if rounding_mode == ROUND: + return round(price, ndigits) + ticks = price * (10**ndigits) + if rounding_mode == ROUND_UP: + return ceil(ticks) / (10**ndigits) + if rounding_mode == TRUNCATE: + return int(ticks) / (10**ndigits) + if rounding_mode == ROUND_DOWN: + return floor(ticks) / (10**ndigits) + raise ValueError(f"Unknown rounding_mode {rounding_mode}") + raise ValueError(f"Unknown precisionMode {precisionMode}") return price diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gate.py similarity index 87% rename from freqtrade/exchange/gateio.py rename to freqtrade/exchange/gate.py index de178af02..2ac135fc1 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gate.py @@ -4,8 +4,7 @@ from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from freqtrade.constants import BuySell -from freqtrade.enums import MarginMode, TradingMode -from freqtrade.exceptions import OperationalException +from freqtrade.enums import MarginMode, PriceType, TradingMode from freqtrade.exchange import Exchange from freqtrade.misc import safe_value_fallback2 @@ -13,7 +12,7 @@ from freqtrade.misc import safe_value_fallback2 logger = logging.getLogger(__name__) -class Gateio(Exchange): +class Gate(Exchange): """ Gate.io exchange class. Contains adjustments needed for Freqtrade to work with this exchange. @@ -28,12 +27,21 @@ class Gateio(Exchange): "order_time_in_force": ['GTC', 'IOC'], "stoploss_order_types": {"limit": "limit"}, "stoploss_on_exchange": True, + "marketOrderRequiresPrice": True, } _ft_has_futures: Dict = { "needs_trading_fees": True, + "marketOrderRequiresPrice": False, + "tickers_have_bid_ask": False, "fee_cost_in_contracts": False, # Set explicitly to false for clarity "order_props_in_contracts": ['amount', 'filled', 'remaining'], + "stop_price_type_field": "price_type", + "stop_price_type_value_mapping": { + PriceType.LAST: 0, + PriceType.MARK: 1, + PriceType.INDEX: 2, + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ @@ -43,13 +51,6 @@ class Gateio(Exchange): (TradingMode.FUTURES, MarginMode.ISOLATED) ] - def validate_ordertypes(self, order_types: Dict) -> None: - - if self.trading_mode != TradingMode.FUTURES: - if any(v == 'market' for k, v in order_types.items()): - raise OperationalException( - f'Exchange {self.name} does not support market orders.') - def _get_params( self, side: BuySell, @@ -67,8 +68,7 @@ class Gateio(Exchange): ) if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES: params['type'] = 'market' - param = self._ft_has.get('time_in_force_parameter', '') - params.update({param: 'IOC'}) + params.update({'timeInForce': 'IOC'}) return params def get_trades_for_order(self, order_id: str, pair: str, since: datetime, @@ -77,7 +77,7 @@ class Gateio(Exchange): if self.trading_mode == TradingMode.FUTURES: # Futures usually don't contain fees in the response. - # As such, futures orders on gateio will not contain a fee, which causes + # As such, futures orders on gate will not contain a fee, which causes # a repeated "update fee" cycle and wrong calculations. # Therefore we patch the response with fees if it's not available. # An alternative also contianing fees would be diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py index a48c9a198..bc4c7aa81 100644 --- a/freqtrade/exchange/hitbtc.py +++ b/freqtrade/exchange/hitbtc.py @@ -19,5 +19,4 @@ class Hitbtc(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, - "ohlcv_params": {"sort": "DESC"} } diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 5d8c1ad29..c41bb6d56 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier +from freqtrade.exchange.exchange_utils import ROUND_DOWN, ROUND_UP from freqtrade.exchange.types import Tickers @@ -97,8 +98,8 @@ class Kraken(Exchange): )) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, - order_types: Dict, side: BuySell, leverage: float) -> Dict: + def create_stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: BuySell, leverage: float) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. @@ -109,6 +110,7 @@ class Kraken(Exchange): if self.trading_mode == TradingMode.FUTURES: params.update({'reduceOnly': True}) + round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP if order_types.get('stoploss', 'market') == 'limit': ordertype = "stop-loss-limit" limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) @@ -116,11 +118,11 @@ class Kraken(Exchange): limit_rate = stop_price * limit_price_pct else: limit_rate = stop_price * (2 - limit_price_pct) - params['price2'] = self.price_to_precision(pair, limit_rate) + params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode) else: ordertype = "stop-loss" - stop_price = self.price_to_precision(pair, stop_price) + stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode) if self._config['dry_run']: dry_order = self.create_dry_run_order( @@ -158,7 +160,7 @@ class Kraken(Exchange): self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None + accept_fail: bool = False, ): """ Kraken set's the leverage as an option in the order object, so we need to diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 6c7d7acfc..20e558513 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -36,3 +36,35 @@ class Kucoin(Exchange): 'stop': 'loss' }) return params + + def create_order( + self, + *, + pair: str, + ordertype: str, + side: BuySell, + amount: float, + rate: float, + leverage: float, + reduceOnly: bool = False, + time_in_force: str = 'GTC', + ) -> Dict: + + res = super().create_order( + pair=pair, + ordertype=ordertype, + side=side, + amount=amount, + rate=rate, + leverage=leverage, + reduceOnly=reduceOnly, + time_in_force=time_in_force, + ) + # Kucoin returns only the order-id. + # ccxt returns status = 'closed' at the moment - which is information ccxt invented. + # Since we rely on status heavily, we must set it to 'open' here. + # ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553) + if not self._config['dry_run']: + res['type'] = ordertype + res['status'] = 'open' + return res diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 6792c2cba..84b7deb7a 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -1,13 +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.exceptions import DDosProtection, OperationalException, TemporaryError +from freqtrade.enums.pricetype import PriceType +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__) @@ -23,10 +26,19 @@ 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, + "stop_price_param": "stopLossPrice", } _ft_has_futures: Dict = { "tickers_have_quoteVolume": False, "fee_cost_in_contracts": True, + "stop_price_type_field": "slTriggerPxType", + "stop_price_type_value_mapping": { + PriceType.LAST: "last", + PriceType.MARK: "index", + PriceType.INDEX: "mark", + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ @@ -114,17 +126,18 @@ 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) - self._api.set_leverage( + res = self._api.set_leverage( leverage=leverage, symbol=pair, params={ "mgnMode": self.margin_mode.value, "posSide": self._get_posSide(side, False), }) + self._log_exchange_response('set_leverage', res) + except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: @@ -148,3 +161,61 @@ 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 = super()._get_stop_params(side, ordertype, 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 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/exchange/types.py b/freqtrade/exchange/types.py index 813b09297..5568e4336 100644 --- a/freqtrade/exchange/types.py +++ b/freqtrade/exchange/types.py @@ -15,6 +15,15 @@ class Ticker(TypedDict): # Several more - only listing required. +class OrderBook(TypedDict): + symbol: str + bids: List[Tuple[float, float]] + asks: List[Tuple[float, float]] + timestamp: Optional[int] + datetime: Optional[str] + nonce: Optional[int] + + Tickers = Dict[str, Ticker] # pair, timeframe, candleType, OHLCV, drop last?, diff --git a/freqtrade/freqai/RL/Base3ActionRLEnv.py b/freqtrade/freqai/RL/Base3ActionRLEnv.py index 3b5fffc58..538ca3a6a 100644 --- a/freqtrade/freqai/RL/Base3ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base3ActionRLEnv.py @@ -1,7 +1,7 @@ import logging from enum import Enum -from gym import spaces +from gymnasium import spaces from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions @@ -47,7 +47,7 @@ class Base3ActionRLEnv(BaseEnvironment): self._update_unrealized_total_profit() step_reward = self.calculate_reward(action) self.total_reward += step_reward - self.tensorboard_log(self.actions._member_names_[action]) + self.tensorboard_log(self.actions._member_names_[action], category="actions") trade_type = None if self.is_tradesignal(action): @@ -66,7 +66,7 @@ class Base3ActionRLEnv(BaseEnvironment): elif action == Actions.Sell.value and not self.can_short: self._update_total_profit() self._position = Positions.Neutral - trade_type = "neutral" + trade_type = "exit" self._last_trade_tick = None else: print("case not defined") @@ -74,7 +74,7 @@ class Base3ActionRLEnv(BaseEnvironment): if trade_type is not None: self.trade_history.append( {'price': self.current_price(), 'index': self._current_tick, - 'type': trade_type}) + 'type': trade_type, 'profit': self.get_unrealized_profit()}) if (self._total_profit < self.max_drawdown or self._total_unrealized_profit < self.max_drawdown): @@ -94,9 +94,12 @@ class Base3ActionRLEnv(BaseEnvironment): observation = self._get_observation() + # user can play with time if they want + truncated = False + self._update_history(info) - return observation, step_reward, self._done, info + return observation, step_reward, self._done, truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/Base4ActionRLEnv.py b/freqtrade/freqai/RL/Base4ActionRLEnv.py index 8f45028b1..12f10d4fc 100644 --- a/freqtrade/freqai/RL/Base4ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base4ActionRLEnv.py @@ -1,7 +1,7 @@ import logging from enum import Enum -from gym import spaces +from gymnasium import spaces from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions @@ -48,20 +48,10 @@ class Base4ActionRLEnv(BaseEnvironment): self._update_unrealized_total_profit() step_reward = self.calculate_reward(action) self.total_reward += step_reward - self.tensorboard_log(self.actions._member_names_[action]) + self.tensorboard_log(self.actions._member_names_[action], category="actions") trade_type = None if self.is_tradesignal(action): - """ - Action: Neutral, position: Long -> Close Long - Action: Neutral, position: Short -> Close Short - - Action: Long, position: Neutral -> Open Long - Action: Long, position: Short -> Close Short and Open Long - - Action: Short, position: Neutral -> Open Short - Action: Short, position: Long -> Close Long and Open Short - """ if action == Actions.Neutral.value: self._position = Positions.Neutral @@ -69,16 +59,16 @@ class Base4ActionRLEnv(BaseEnvironment): self._last_trade_tick = None elif action == Actions.Long_enter.value: self._position = Positions.Long - trade_type = "long" + trade_type = "enter_long" self._last_trade_tick = self._current_tick elif action == Actions.Short_enter.value: self._position = Positions.Short - trade_type = "short" + trade_type = "enter_short" self._last_trade_tick = self._current_tick elif action == Actions.Exit.value: self._update_total_profit() self._position = Positions.Neutral - trade_type = "neutral" + trade_type = "exit" self._last_trade_tick = None else: print("case not defined") @@ -86,7 +76,7 @@ class Base4ActionRLEnv(BaseEnvironment): if trade_type is not None: self.trade_history.append( {'price': self.current_price(), 'index': self._current_tick, - 'type': trade_type}) + 'type': trade_type, 'profit': self.get_unrealized_profit()}) if (self._total_profit < self.max_drawdown or self._total_unrealized_profit < self.max_drawdown): @@ -106,9 +96,12 @@ class Base4ActionRLEnv(BaseEnvironment): observation = self._get_observation() + # user can play with time if they want + truncated = False + self._update_history(info) - return observation, step_reward, self._done, info + return observation, step_reward, self._done, truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/Base5ActionRLEnv.py b/freqtrade/freqai/RL/Base5ActionRLEnv.py index 22d3cae30..35d04f942 100644 --- a/freqtrade/freqai/RL/Base5ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base5ActionRLEnv.py @@ -1,7 +1,7 @@ import logging from enum import Enum -from gym import spaces +from gymnasium import spaces from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment, Positions @@ -49,20 +49,10 @@ class Base5ActionRLEnv(BaseEnvironment): self._update_unrealized_total_profit() step_reward = self.calculate_reward(action) self.total_reward += step_reward - self.tensorboard_log(self.actions._member_names_[action]) + self.tensorboard_log(self.actions._member_names_[action], category="actions") trade_type = None if self.is_tradesignal(action): - """ - Action: Neutral, position: Long -> Close Long - Action: Neutral, position: Short -> Close Short - - Action: Long, position: Neutral -> Open Long - Action: Long, position: Short -> Close Short and Open Long - - Action: Short, position: Neutral -> Open Short - Action: Short, position: Long -> Close Long and Open Short - """ if action == Actions.Neutral.value: self._position = Positions.Neutral @@ -70,21 +60,21 @@ class Base5ActionRLEnv(BaseEnvironment): self._last_trade_tick = None elif action == Actions.Long_enter.value: self._position = Positions.Long - trade_type = "long" + trade_type = "enter_long" self._last_trade_tick = self._current_tick elif action == Actions.Short_enter.value: self._position = Positions.Short - trade_type = "short" + trade_type = "enter_short" self._last_trade_tick = self._current_tick elif action == Actions.Long_exit.value: self._update_total_profit() self._position = Positions.Neutral - trade_type = "neutral" + trade_type = "exit_long" self._last_trade_tick = None elif action == Actions.Short_exit.value: self._update_total_profit() self._position = Positions.Neutral - trade_type = "neutral" + trade_type = "exit_short" self._last_trade_tick = None else: print("case not defined") @@ -92,7 +82,7 @@ class Base5ActionRLEnv(BaseEnvironment): if trade_type is not None: self.trade_history.append( {'price': self.current_price(), 'index': self._current_tick, - 'type': trade_type}) + 'type': trade_type, 'profit': self.get_unrealized_profit()}) if (self._total_profit < self.max_drawdown or self._total_unrealized_profit < self.max_drawdown): @@ -111,10 +101,12 @@ class Base5ActionRLEnv(BaseEnvironment): ) observation = self._get_observation() + # user can play with time if they want + truncated = False self._update_history(info) - return observation, step_reward, self._done, info + return observation, step_reward, self._done, truncated, info def is_tradesignal(self, action: int) -> bool: """ diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index ef1c02a3b..7c83a7e42 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -4,11 +4,11 @@ from abc import abstractmethod from enum import Enum from typing import Optional, Type, Union -import gym +import gymnasium as gym import numpy as np import pandas as pd -from gym import spaces -from gym.utils import seeding +from gymnasium import spaces +from gymnasium.utils import seeding from pandas import DataFrame @@ -45,7 +45,8 @@ class BaseEnvironment(gym.Env): def __init__(self, df: DataFrame = DataFrame(), prices: DataFrame = DataFrame(), reward_kwargs: dict = {}, window_size=10, starting_point=True, id: str = 'baseenv-1', seed: int = 1, config: dict = {}, live: bool = False, - fee: float = 0.0015, can_short: bool = False): + fee: float = 0.0015, can_short: bool = False, pair: str = "", + df_raw: DataFrame = DataFrame()): """ Initializes the training/eval environment. :param df: dataframe of features @@ -60,12 +61,14 @@ class BaseEnvironment(gym.Env): :param fee: The fee to use for environmental interactions. :param can_short: Whether or not the environment can short """ - self.config = config - self.rl_config = config['freqai']['rl_config'] - self.add_state_info = self.rl_config.get('add_state_info', False) - self.id = id - self.max_drawdown = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8) - self.compound_trades = config['stake_amount'] == 'unlimited' + self.config: dict = config + self.rl_config: dict = config['freqai']['rl_config'] + self.add_state_info: bool = self.rl_config.get('add_state_info', False) + self.id: str = id + self.max_drawdown: float = 1 - self.rl_config.get('max_training_drawdown_pct', 0.8) + self.compound_trades: bool = config['stake_amount'] == 'unlimited' + self.pair: str = pair + self.raw_features: DataFrame = df_raw if self.config.get('fee', None) is not None: self.fee = self.config['fee'] else: @@ -74,8 +77,8 @@ class BaseEnvironment(gym.Env): # set here to default 5Ac, but all children envs can override this self.actions: Type[Enum] = BaseActions self.tensorboard_metrics: dict = {} - self.can_short = can_short - self.live = live + self.can_short: bool = can_short + self.live: bool = live if not self.live and self.add_state_info: self.add_state_info = False logger.warning("add_state_info is not available in backtesting. Deactivating.") @@ -93,13 +96,12 @@ class BaseEnvironment(gym.Env): :param reward_kwargs: extra config settings assigned by user in `rl_config` :param starting_point: start at edge of window or not """ - self.df = df - self.signal_features = self.df - self.prices = prices - self.window_size = window_size - self.starting_point = starting_point - self.rr = reward_kwargs["rr"] - self.profit_aim = reward_kwargs["profit_aim"] + self.signal_features: DataFrame = df + self.prices: DataFrame = prices + self.window_size: int = window_size + self.starting_point: bool = starting_point + self.rr: float = reward_kwargs["rr"] + self.profit_aim: float = reward_kwargs["profit_aim"] # # spaces if self.add_state_info: @@ -125,6 +127,14 @@ class BaseEnvironment(gym.Env): self.history: dict = {} self.trade_history: list = [] + def get_attr(self, attr: str): + """ + Returns the attribute of the environment + :param attr: attribute to return + :return: attribute + """ + return getattr(self, attr) + @abstractmethod def set_action_space(self): """ @@ -135,7 +145,8 @@ class BaseEnvironment(gym.Env): self.np_random, seed = seeding.np_random(seed) return [seed] - def tensorboard_log(self, metric: str, value: Union[int, float] = 1, inc: bool = True): + def tensorboard_log(self, metric: str, value: Optional[Union[int, float]] = None, + inc: Optional[bool] = None, category: str = "custom"): """ Function builds the tensorboard_metrics dictionary to be parsed by the TensorboardCallback. This @@ -147,17 +158,24 @@ class BaseEnvironment(gym.Env): def calculate_reward(self, action: int) -> float: if not self._is_valid(action): - self.tensorboard_log("is_valid") + self.tensorboard_log("invalid") return -2 :param metric: metric to be tracked and incremented - :param value: value to increment `metric` by - :param inc: sets whether the `value` is incremented or not + :param value: `metric` value + :param inc: (deprecated) sets whether the `value` is incremented or not + :param category: `metric` category """ - if not inc or metric not in self.tensorboard_metrics: - self.tensorboard_metrics[metric] = value + increment = True if value is None else False + value = 1 if increment else value + + if category not in self.tensorboard_metrics: + self.tensorboard_metrics[category] = {} + + if not increment or metric not in self.tensorboard_metrics[category]: + self.tensorboard_metrics[category][metric] = value else: - self.tensorboard_metrics[metric] += value + self.tensorboard_metrics[category][metric] += value def reset_tensorboard_log(self): self.tensorboard_metrics = {} @@ -193,7 +211,7 @@ class BaseEnvironment(gym.Env): self.close_trade_profit = [] self._total_unrealized_profit = 1 - return self._get_observation() + return self._get_observation(), self.history @abstractmethod def step(self, action: int): @@ -288,6 +306,12 @@ class BaseEnvironment(gym.Env): """ An example reward function. This is the one function that users will likely wish to inject their own creativity into. + + Warning! + This is function is a showcase of functionality designed to show as many possible + environment control features as possible. It is also designed to run quickly + on small computers. This is a benchmark, it is *not* for live production. + :param action: int = The action made by the agent for the current candle. :return: float = the reward to give to the agent for current step (used for optimization diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index 3a4d0d0e6..8ee3c7c56 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -1,3 +1,4 @@ +import copy import importlib import logging from abc import abstractmethod @@ -5,7 +6,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Any, Callable, Dict, Optional, Tuple, Type, Union -import gym +import gymnasium as gym import numpy as np import numpy.typing as npt import pandas as pd @@ -15,14 +16,14 @@ from pandas import DataFrame from stable_baselines3.common.callbacks import EvalCallback from stable_baselines3.common.monitor import Monitor from stable_baselines3.common.utils import set_random_seed -from stable_baselines3.common.vec_env import SubprocVecEnv +from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor from freqtrade.exceptions import OperationalException from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.freqai_interface import IFreqaiModel from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv -from freqtrade.freqai.RL.BaseEnvironment import BaseActions, Positions -from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback +from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment, Positions +from freqtrade.freqai.tensorboard.TensorboardCallback import TensorboardCallback from freqtrade.persistence import Trade @@ -45,11 +46,12 @@ class BaseReinforcementLearningModel(IFreqaiModel): 'cpu_count', 1), max(int(self.max_system_threads / 2), 1)) th.set_num_threads(self.max_threads) self.reward_params = self.freqai_info['rl_config']['model_reward_parameters'] - self.train_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env() - self.eval_env: Union[SubprocVecEnv, Type[gym.Env]] = gym.Env() + self.train_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env() + self.eval_env: Union[VecMonitor, SubprocVecEnv, gym.Env] = gym.Env() self.eval_callback: Optional[EvalCallback] = None self.model_type = self.freqai_info['rl_config']['model_type'] self.rl_config = self.freqai_info['rl_config'] + self.df_raw: DataFrame = DataFrame() self.continual_learning = self.freqai_info.get('continual_learning', False) if self.model_type in SB3_MODELS: import_str = 'stable_baselines3' @@ -107,10 +109,12 @@ class BaseReinforcementLearningModel(IFreqaiModel): data_dictionary: Dict[str, Any] = dk.make_train_test_datasets( features_filtered, labels_filtered) + self.df_raw = copy.deepcopy(data_dictionary["train_features"]) dk.fit_labels() # FIXME useless for now, but just satiating append methods # normalize all data based on train_dataset only prices_train, prices_test = self.build_ohlc_price_dataframes(dk.data_dictionary, pair, dk) + data_dictionary = dk.normalize_data(data_dictionary) # data cleaning/analysis @@ -143,14 +147,10 @@ class BaseReinforcementLearningModel(IFreqaiModel): train_df = data_dictionary["train_features"] test_df = data_dictionary["test_features"] - env_info = self.pack_env_dict() + env_info = self.pack_env_dict(dk.pair) - self.train_env = self.MyRLEnv(df=train_df, - prices=prices_train, - **env_info) - self.eval_env = Monitor(self.MyRLEnv(df=test_df, - prices=prices_test, - **env_info)) + self.train_env = self.MyRLEnv(df=train_df, prices=prices_train, **env_info) + self.eval_env = Monitor(self.MyRLEnv(df=test_df, prices=prices_test, **env_info)) self.eval_callback = EvalCallback(self.eval_env, deterministic=True, render=False, eval_freq=len(train_df), best_model_save_path=str(dk.data_path)) @@ -158,7 +158,7 @@ class BaseReinforcementLearningModel(IFreqaiModel): actions = self.train_env.get_actions() self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions) - def pack_env_dict(self) -> Dict[str, Any]: + def pack_env_dict(self, pair: str) -> Dict[str, Any]: """ Create dictionary of environment arguments """ @@ -166,7 +166,9 @@ class BaseReinforcementLearningModel(IFreqaiModel): "reward_kwargs": self.reward_params, "config": self.config, "live": self.live, - "can_short": self.can_short} + "can_short": self.can_short, + "pair": pair, + "df_raw": self.df_raw} if self.data_provider: env_info["fee"] = self.data_provider._exchange \ .get_fee(symbol=self.data_provider.current_whitelist()[0]) # type: ignore @@ -233,6 +235,9 @@ class BaseReinforcementLearningModel(IFreqaiModel): filtered_dataframe, _ = dk.filter_features( unfiltered_df, dk.training_features_list, training_filter=False ) + + filtered_dataframe = self.drop_ohlc_from_df(filtered_dataframe, dk) + filtered_dataframe = dk.normalize_data_from_metadata(filtered_dataframe) dk.data_dictionary["prediction_features"] = filtered_dataframe @@ -280,7 +285,6 @@ class BaseReinforcementLearningModel(IFreqaiModel): train_df = data_dictionary["train_features"] test_df = data_dictionary["test_features"] - # %-raw_volume_gen_shift-2_ETH/USDT_1h # price data for model training and evaluation tf = self.config['timeframe'] rename_dict = {'%-raw_open': 'open', '%-raw_low': 'low', @@ -313,8 +317,24 @@ class BaseReinforcementLearningModel(IFreqaiModel): prices_test.rename(columns=rename_dict, inplace=True) prices_test.reset_index(drop=True) + train_df = self.drop_ohlc_from_df(train_df, dk) + test_df = self.drop_ohlc_from_df(test_df, dk) + return prices_train, prices_test + def drop_ohlc_from_df(self, df: DataFrame, dk: FreqaiDataKitchen): + """ + Given a dataframe, drop the ohlc data + """ + drop_list = ['%-raw_open', '%-raw_low', '%-raw_high', '%-raw_close'] + + if self.rl_config["drop_ohlc_from_features"]: + df.drop(drop_list, axis=1, inplace=True) + feature_list = dk.training_features_list + dk.training_features_list = [e for e in feature_list if e not in drop_list] + + return df + def load_model_from_disk(self, dk: FreqaiDataKitchen) -> Any: """ Can be used by user if they are trying to limit_ram_usage *and* @@ -347,10 +367,16 @@ class BaseReinforcementLearningModel(IFreqaiModel): sets a custom reward based on profit and trade duration. """ - def calculate_reward(self, action: int) -> float: + def calculate_reward(self, action: int) -> float: # noqa: C901 """ An example reward function. This is the one function that users will likely wish to inject their own creativity into. + + Warning! + This is function is a showcase of functionality designed to show as many possible + environment control features as possible. It is also designed to run quickly + on small computers. This is a benchmark, it is *not* for live production. + :param action: int = The action made by the agent for the current candle. :return: float = the reward to give to the agent for current step (used for optimization @@ -363,10 +389,19 @@ class BaseReinforcementLearningModel(IFreqaiModel): pnl = self.get_unrealized_profit() factor = 100. + # you can use feature values from dataframe + rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{self.pair}_" + f"{self.config['timeframe']}"].iloc[self._current_tick] + # reward agent for entering trades if (action in (Actions.Long_enter.value, Actions.Short_enter.value) and self._position == Positions.Neutral): - return 25 + if rsi_now < 40: + factor = 40 / rsi_now + else: + factor = 1 + return 25 * factor + # discourage agent from not entering trades if action == Actions.Neutral.value and self._position == Positions.Neutral: return -1 @@ -402,9 +437,8 @@ class BaseReinforcementLearningModel(IFreqaiModel): return 0. -def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, +def make_env(MyRLEnv: Type[BaseEnvironment], env_id: str, rank: int, seed: int, train_df: DataFrame, price: DataFrame, - monitor: bool = False, env_info: Dict[str, Any] = {}) -> Callable: """ Utility function for multiprocessed env. @@ -421,8 +455,7 @@ def make_env(MyRLEnv: Type[gym.Env], env_id: str, rank: int, env = MyRLEnv(df=train_df, prices=price, id=env_id, seed=seed + rank, **env_info) - if monitor: - env = Monitor(env) + return env set_random_seed(seed) return _init diff --git a/freqtrade/freqai/base_models/BasePyTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py new file mode 100644 index 000000000..436294dcc --- /dev/null +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -0,0 +1,151 @@ +import logging +from typing import Dict, List, Tuple + +import numpy as np +import numpy.typing as npt +import pandas as pd +import torch +from pandas import DataFrame +from torch.nn import functional as F + +from freqtrade.exceptions import OperationalException +from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + + +logger = logging.getLogger(__name__) + + +class BasePyTorchClassifier(BasePyTorchModel): + """ + A PyTorch implementation of a classifier. + User must implement fit method + + Important! + + - User must declare the target class names in the strategy, + under IStrategy.set_freqai_targets method. + + for example, in your strategy: + ``` + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + self.freqai.class_names = ["down", "up"] + dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > + dataframe["close"], 'up', 'down') + + return dataframe + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.class_name_to_index = None + self.index_to_class_name = None + + def predict( + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param dk: dk: The datakitchen object + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + :raises ValueError: if 'class_names' doesn't exist in model meta_data. + """ + + class_names = self.model.model_meta_data.get("class_names", None) + if not class_names: + raise ValueError( + "Missing class names. " + "self.model.model_meta_data['class_names'] is None." + ) + + if not self.class_name_to_index: + self.init_class_names_to_index_mapping(class_names) + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + self.data_cleaning_predict(dk) + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) + self.model.model.eval() + logits = self.model.model(x) + probs = F.softmax(logits, dim=-1) + predicted_classes = torch.argmax(probs, dim=-1) + predicted_classes_str = self.decode_class_names(predicted_classes) + # used .tolist to convert probs into an iterable, in this way Tensors + # are automatically moved to the CPU first if necessary. + pred_df_prob = DataFrame(probs.detach().tolist(), columns=class_names) + pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) + pred_df = pd.concat([pred_df, pred_df_prob], axis=1) + return (pred_df, dk.do_predict) + + def encode_class_names( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str], + ): + """ + encode class name, str -> int + assuming first column of *_labels data frame to be the target column + containing the class names + """ + + target_column_name = dk.label_list[0] + for split in self.splits: + label_df = data_dictionary[f"{split}_labels"] + self.assert_valid_class_names(label_df[target_column_name], class_names) + label_df[target_column_name] = list( + map(lambda x: self.class_name_to_index[x], label_df[target_column_name]) + ) + + @staticmethod + def assert_valid_class_names( + target_column: pd.Series, + class_names: List[str] + ): + non_defined_labels = set(target_column) - set(class_names) + if len(non_defined_labels) != 0: + raise OperationalException( + f"Found non defined labels: {non_defined_labels}, ", + f"expecting labels: {class_names}" + ) + + def decode_class_names(self, class_ints: torch.Tensor) -> List[str]: + """ + decode class name, int -> str + """ + + return list(map(lambda x: self.index_to_class_name[x.item()], class_ints)) + + def init_class_names_to_index_mapping(self, class_names): + self.class_name_to_index = {s: i for i, s in enumerate(class_names)} + self.index_to_class_name = {i: s for i, s in enumerate(class_names)} + logger.info(f"encoded class name to index: {self.class_name_to_index}") + + def convert_label_column_to_int( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str] + ): + self.init_class_names_to_index_mapping(class_names) + self.encode_class_names(data_dictionary, dk, class_names) + + def get_class_names(self) -> List[str]: + if not self.class_names: + raise ValueError( + "self.class_names is empty, " + "set self.freqai.class_names = ['class a', 'class b', 'class c'] " + "inside IStrategy.set_freqai_targets method." + ) + + return self.class_names diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py new file mode 100644 index 000000000..82042d24c --- /dev/null +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -0,0 +1,84 @@ +import logging +from abc import ABC, abstractmethod +from time import time +from typing import Any + +import torch +from pandas import DataFrame + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.freqai_interface import IFreqaiModel +from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor + + +logger = logging.getLogger(__name__) + + +class BasePyTorchModel(IFreqaiModel, ABC): + """ + Base class for PyTorch type models. + User *must* inherit from this class and set fit() and predict() and + data_convertor property. + """ + + def __init__(self, **kwargs): + super().__init__(config=kwargs["config"]) + self.dd.model_type = "pytorch" + self.device = "cuda" if torch.cuda.is_available() else "cpu" + test_size = self.freqai_info.get('data_split_parameters', {}).get('test_size') + self.splits = ["train", "test"] if test_size != 0 else ["train"] + self.window_size = self.freqai_info.get("conv_width", 1) + + def train( + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + ) -> Any: + """ + Filter the training data and train a model to it. Train makes heavy use of the datakitchen + for storing, saving, loading, and analyzing the data. + :param unfiltered_df: Full dataframe for the current training period + :return: + :model: Trained model which can be used to inference (self.predict) + """ + + logger.info(f"-------------------- Starting training {pair} --------------------") + + start_time = time() + + features_filtered, labels_filtered = dk.filter_features( + unfiltered_df, + dk.training_features_list, + dk.label_list, + training_filter=True, + ) + + # split data into train/test data. + data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) + if not self.freqai_info.get("fit_live_predictions", 0) or not self.live: + dk.fit_labels() + # normalize all data based on train_dataset only + data_dictionary = dk.normalize_data(data_dictionary) + + # optional additional data cleaning/analysis + self.data_cleaning_train(dk) + + logger.info( + f"Training model on {len(dk.data_dictionary['train_features'].columns)} features" + ) + logger.info(f"Training model on {len(data_dictionary['train_features'])} data points") + + model = self.fit(data_dictionary, dk) + end_time = time() + + logger.info(f"-------------------- Done training {pair} " + f"({end_time - start_time:.2f} secs) --------------------") + + return model + + @property + @abstractmethod + def data_convertor(self) -> PyTorchDataConvertor: + """ + a class responsible for converting `*_features` & `*_labels` pandas dataframes + to pytorch tensors. + """ + raise NotImplementedError("Abstract property") diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py new file mode 100644 index 000000000..6139f2e85 --- /dev/null +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -0,0 +1,51 @@ +import logging +from typing import Tuple + +import numpy as np +import numpy.typing as npt +from pandas import DataFrame + +from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + + +logger = logging.getLogger(__name__) + + +class BasePyTorchRegressor(BasePyTorchModel): + """ + A PyTorch implementation of a regressor. + User must implement fit method + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def predict( + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + """ + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + + self.data_cleaning_predict(dk) + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) + self.model.model.eval() + y = self.model.model(x) + pred_df = DataFrame(y.detach().tolist(), columns=[dk.label_list[0]]) + pred_df = dk.denormalize_labels_from_metadata(pred_df) + return (pred_df, dk.do_predict) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 848fb20eb..b68a9dcad 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -59,7 +59,7 @@ class FreqaiDataDrawer: Juha Nykänen @suikula, Wagner Costa @wagnercosta, Johan Vlugt @Jooopieeert """ - def __init__(self, full_path: Path, config: Config, follow_mode: bool = False): + def __init__(self, full_path: Path, config: Config): self.config = config self.freqai_info = config.get("freqai", {}) @@ -72,21 +72,13 @@ class FreqaiDataDrawer: self.model_return_values: Dict[str, DataFrame] = {} self.historic_data: Dict[str, Dict[str, DataFrame]] = {} self.historic_predictions: Dict[str, DataFrame] = {} - self.follower_dict: Dict[str, pair_info] = {} self.full_path = full_path - self.follower_name: str = self.config.get("bot_name", "follower1") - self.follower_dict_path = Path( - self.full_path / f"follower_dictionary-{self.follower_name}.json" - ) self.historic_predictions_path = Path(self.full_path / "historic_predictions.pkl") self.historic_predictions_bkp_path = Path( self.full_path / "historic_predictions.backup.pkl") self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json") self.global_metadata_path = Path(self.full_path / "global_metadata.json") self.metric_tracker_path = Path(self.full_path / "metric_tracker.json") - self.follow_mode = follow_mode - if follow_mode: - self.create_follower_dict() self.load_drawer_from_disk() self.load_historic_predictions_from_disk() self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {} @@ -134,7 +126,7 @@ class FreqaiDataDrawer: """ exists = self.global_metadata_path.is_file() if exists: - with open(self.global_metadata_path, "r") as fp: + with self.global_metadata_path.open("r") as fp: metatada_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) return metatada_dict return {} @@ -147,15 +139,10 @@ class FreqaiDataDrawer: """ exists = self.pair_dictionary_path.is_file() if exists: - with open(self.pair_dictionary_path, "r") as fp: + with self.pair_dictionary_path.open("r") as fp: self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) - elif not self.follow_mode: - logger.info("Could not find existing datadrawer, starting from scratch") else: - logger.warning( - f"Follower could not find pair_dictionary at {self.full_path} " - "sending null values back to strategy" - ) + logger.info("Could not find existing datadrawer, starting from scratch") def load_metric_tracker_from_disk(self): """ @@ -165,7 +152,7 @@ class FreqaiDataDrawer: if self.freqai_info.get('write_metrics_to_disk', False): exists = self.metric_tracker_path.is_file() if exists: - with open(self.metric_tracker_path, "r") as fp: + with self.metric_tracker_path.open("r") as fp: self.metric_tracker = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) logger.info("Loading existing metric tracker from disk.") else: @@ -179,7 +166,7 @@ class FreqaiDataDrawer: exists = self.historic_predictions_path.is_file() if exists: try: - with open(self.historic_predictions_path, "rb") as fp: + with self.historic_predictions_path.open("rb") as fp: self.historic_predictions = cloudpickle.load(fp) logger.info( f"Found existing historic predictions at {self.full_path}, but beware " @@ -189,17 +176,12 @@ class FreqaiDataDrawer: except EOFError: logger.warning( 'Historical prediction file was corrupted. Trying to load backup file.') - with open(self.historic_predictions_bkp_path, "rb") as fp: + with self.historic_predictions_bkp_path.open("rb") as fp: self.historic_predictions = cloudpickle.load(fp) logger.warning('FreqAI successfully loaded the backup historical predictions file.') - elif not self.follow_mode: - logger.info("Could not find existing historic_predictions, starting from scratch") else: - logger.warning( - f"Follower could not find historic predictions at {self.full_path} " - "sending null values back to strategy" - ) + logger.info("Could not find existing historic_predictions, starting from scratch") return exists @@ -207,7 +189,7 @@ class FreqaiDataDrawer: """ Save historic predictions pickle to disk """ - with open(self.historic_predictions_path, "wb") as fp: + with self.historic_predictions_path.open("wb") as fp: cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL) # create a backup @@ -218,58 +200,33 @@ class FreqaiDataDrawer: Save metric tracker of all pair metrics collected. """ with self.save_lock: - with open(self.metric_tracker_path, 'w') as fp: + with self.metric_tracker_path.open('w') as fp: rapidjson.dump(self.metric_tracker, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) - def save_drawer_to_disk(self): + def save_drawer_to_disk(self) -> None: """ Save data drawer full of all pair model metadata in present model folder. """ with self.save_lock: - with open(self.pair_dictionary_path, 'w') as fp: + with self.pair_dictionary_path.open('w') as fp: rapidjson.dump(self.pair_dict, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) - def save_follower_dict_to_disk(self): - """ - Save follower dictionary to disk (used by strategy for persistent prediction targets) - """ - with open(self.follower_dict_path, "w") as fp: - rapidjson.dump(self.follower_dict, fp, default=self.np_encoder, - number_mode=rapidjson.NM_NATIVE) - def save_global_metadata_to_disk(self, metadata: Dict[str, Any]): """ Save global metadata json to disk """ with self.save_lock: - with open(self.global_metadata_path, 'w') as fp: + with self.global_metadata_path.open('w') as fp: rapidjson.dump(metadata, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) - def create_follower_dict(self): - """ - Create or dictionary for each follower to maintain unique persistent prediction targets - """ - - whitelist_pairs = self.config.get("exchange", {}).get("pair_whitelist") - - exists = self.follower_dict_path.is_file() - - if exists: - logger.info("Found an existing follower dictionary") - - for pair in whitelist_pairs: - self.follower_dict[pair] = {} - - self.save_follower_dict_to_disk() - def np_encoder(self, object): if isinstance(object, np.generic): return object.item() - def get_pair_dict_info(self, pair: str) -> Tuple[str, int, bool]: + def get_pair_dict_info(self, pair: str) -> Tuple[str, int]: """ Locate and load existing model metadata from persistent storage. If not located, create a new one and append the current pair to it and prepare it for its first @@ -278,32 +235,19 @@ class FreqaiDataDrawer: :return: model_filename: str = unique filename used for loading persistent objects from disk trained_timestamp: int = the last time the coin was trained - return_null_array: bool = Follower could not find pair metadata """ pair_dict = self.pair_dict.get(pair) - data_path_set = self.pair_dict.get(pair, self.empty_pair_dict).get("data_path", "") - return_null_array = False if pair_dict: model_filename = pair_dict["model_filename"] trained_timestamp = pair_dict["trained_timestamp"] - elif not self.follow_mode: + else: self.pair_dict[pair] = self.empty_pair_dict.copy() model_filename = "" trained_timestamp = 0 - if not data_path_set and self.follow_mode: - logger.warning( - f"Follower could not find current pair {pair} in " - f"pair_dictionary at path {self.full_path}, sending null values " - "back to strategy." - ) - trained_timestamp = 0 - model_filename = '' - return_null_array = True - - return model_filename, trained_timestamp, return_null_array + return model_filename, trained_timestamp def set_pair_dict_info(self, metadata: dict) -> None: pair_in_dict = self.pair_dict.get(metadata["pair"]) @@ -311,7 +255,6 @@ class FreqaiDataDrawer: return else: self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy() - return def set_initial_return_values(self, pair: str, pred_df: DataFrame) -> None: @@ -423,6 +366,12 @@ class FreqaiDataDrawer: def purge_old_models(self) -> None: + num_keep = self.freqai_info["purge_old_models"] + if not num_keep: + return + elif type(num_keep) == bool: + num_keep = 2 + model_folders = [x for x in self.full_path.iterdir() if x.is_dir()] pattern = re.compile(r"sub-train-(\w+)_(\d{10})") @@ -445,11 +394,11 @@ class FreqaiDataDrawer: delete_dict[coin]["timestamps"][int(timestamp)] = dir for coin in delete_dict: - if delete_dict[coin]["num_folders"] > 2: + if delete_dict[coin]["num_folders"] > num_keep: sorted_dict = collections.OrderedDict( sorted(delete_dict[coin]["timestamps"].items()) ) - num_delete = len(sorted_dict) - 2 + num_delete = len(sorted_dict) - num_keep deleted = 0 for k, v in sorted_dict.items(): if deleted >= num_delete: @@ -458,12 +407,6 @@ class FreqaiDataDrawer: shutil.rmtree(v) deleted += 1 - def update_follower_metadata(self): - # follower needs to load from disk to get any changes made by leader to pair_dict - self.load_drawer_from_disk() - if self.config.get("freqai", {}).get("purge_old_models", False): - self.purge_old_models() - def save_metadata(self, dk: FreqaiDataKitchen) -> None: """ Saves only metadata for backtesting studies if user prefers @@ -481,7 +424,7 @@ class FreqaiDataDrawer: dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns) dk.data["label_list"] = dk.label_list - with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp: + with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp: rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) return @@ -503,7 +446,7 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type: + elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -514,7 +457,7 @@ class FreqaiDataDrawer: dk.data["training_features_list"] = dk.training_features_list dk.data["label_list"] = dk.label_list # store the metadata - with open(save_path / f"{dk.model_filename}_metadata.json", "w") as fp: + with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp: rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE) # save the train data to file so we can check preds for area of applicability later @@ -528,7 +471,7 @@ class FreqaiDataDrawer: if self.freqai_info["feature_parameters"].get("principal_component_analysis"): cloudpickle.dump( - dk.pca, open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "wb") + dk.pca, (dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("wb") ) self.model_dictionary[coin] = model @@ -548,12 +491,12 @@ class FreqaiDataDrawer: Load only metadata into datakitchen to increase performance during presaved backtesting (prediction file loading). """ - with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp: + with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp: dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) dk.training_features_list = dk.data["training_features_list"] dk.label_list = dk.data["label_list"] - def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: + def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: # noqa: C901 """ loads all data required to make a prediction on a sub-train time range :returns: @@ -571,7 +514,7 @@ class FreqaiDataDrawer: dk.data = self.meta_data_dictionary[coin]["meta_data"] dk.data_dictionary["train_features"] = self.meta_data_dictionary[coin]["train_df"] else: - with open(dk.data_path / f"{dk.model_filename}_metadata.json", "r") as fp: + with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp: dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE) dk.data_dictionary["train_features"] = pd.read_pickle( @@ -594,6 +537,11 @@ class FreqaiDataDrawer: self.model_type, self.freqai_info['rl_config']['model_type']) MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type']) model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model") + elif self.model_type == 'pytorch': + import torch + zip = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") + model = zip["pytrainer"] + model = model.load_from_checkpoint(zip) if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file(): dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib") @@ -609,7 +557,7 @@ class FreqaiDataDrawer: if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]: dk.pca = cloudpickle.load( - open(dk.data_path / f"{dk.model_filename}_pca_object.pkl", "rb") + (dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("rb") ) return model @@ -627,12 +575,12 @@ class FreqaiDataDrawer: for pair in dk.all_pairs: for tf in feat_params.get("include_timeframes"): - + hist_df = history_data[pair][tf] # check if newest candle is already appended df_dp = strategy.dp.get_pair_dataframe(pair, tf) if len(df_dp.index) == 0: continue - if str(history_data[pair][tf].iloc[-1]["date"]) == str( + if str(hist_df.iloc[-1]["date"]) == str( df_dp.iloc[-1:]["date"].iloc[-1] ): continue @@ -640,21 +588,30 @@ class FreqaiDataDrawer: try: index = ( df_dp.loc[ - df_dp["date"] == history_data[pair][tf].iloc[-1]["date"] + df_dp["date"] == hist_df.iloc[-1]["date"] ].index[0] + 1 ) except IndexError: - logger.warning( - f"Unable to update pair history for {pair}. " - "If this does not resolve itself after 1 additional candle, " - "please report the error to #freqai discord channel" - ) - return + if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]: + raise OperationalException("In memory historical data is older than " + f"oldest DataProvider candle for {pair} on " + f"timeframe {tf}") + else: + index = -1 + logger.warning( + f"No common dates in historical data and dataprovider for {pair}. " + f"Appending latest dataprovider candle to historical data " + "but please be aware that there is likely a gap in the historical " + "data. \n" + f"Historical data ends at {hist_df.iloc[-1]['date']} " + f"while dataprovider starts at {df_dp['date'].iloc[0]} and" + f"ends at {df_dp['date'].iloc[0]}." + ) history_data[pair][tf] = pd.concat( [ - history_data[pair][tf], + hist_df, df_dp.iloc[index:], ], ignore_index=True, diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 9fdc2c98e..21b41db2d 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1,11 +1,12 @@ import copy import inspect import logging +import random import shutil from datetime import datetime, timezone from math import cos, sin from pathlib import Path -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple import numpy as np import numpy.typing as npt @@ -112,7 +113,7 @@ class FreqaiDataKitchen: def set_paths( self, pair: str, - trained_timestamp: int = None, + trained_timestamp: Optional[int] = None, ) -> None: """ Set the paths to the data for the present coin/botloop @@ -170,6 +171,19 @@ class FreqaiDataKitchen: train_labels = labels train_weights = weights + if feat_dict["shuffle_after_split"]: + rint1 = random.randint(0, 100) + rint2 = random.randint(0, 100) + train_features = train_features.sample( + frac=1, random_state=rint1).reset_index(drop=True) + train_labels = train_labels.sample(frac=1, random_state=rint1).reset_index(drop=True) + train_weights = pd.DataFrame(train_weights).sample( + frac=1, random_state=rint1).reset_index(drop=True).to_numpy()[:, 0] + test_features = test_features.sample(frac=1, random_state=rint2).reset_index(drop=True) + test_labels = test_labels.sample(frac=1, random_state=rint2).reset_index(drop=True) + test_weights = pd.DataFrame(test_weights).sample( + frac=1, random_state=rint2).reset_index(drop=True).to_numpy()[:, 0] + # Simplest way to reverse the order of training and test data: if self.freqai_config['feature_parameters'].get('reverse_train_test_order', False): return self.build_data_dictionary( @@ -237,7 +251,7 @@ class FreqaiDataKitchen: (drop_index == 0) & (drop_index_labels == 0) ] logger.info( - f"dropped {len(unfiltered_df) - len(filtered_df)} training points" + f"{self.pair}: dropped {len(unfiltered_df) - len(filtered_df)} training points" f" due to NaNs in populated dataset {len(unfiltered_df)}." ) if (1 - len(filtered_df) / len(unfiltered_df)) > 0.1 and self.live: @@ -661,7 +675,7 @@ class FreqaiDataKitchen: ] logger.info( - f"SVM tossed {len(y_pred) - kept_points.sum()}" + f"{self.pair}: SVM tossed {len(y_pred) - kept_points.sum()}" f" test points from {len(y_pred)} total points." ) @@ -935,7 +949,7 @@ class FreqaiDataKitchen: if (len(do_predict) - do_predict.sum()) > 0: logger.info( - f"DI tossed {len(do_predict) - do_predict.sum()} predictions for " + f"{self.pair}: DI tossed {len(do_predict) - do_predict.sum()} predictions for " "being too far from training data." ) @@ -1247,17 +1261,19 @@ class FreqaiDataKitchen: tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes") for tf in tfs: + metadata = {"pair": pair, "tf": tf} informative_df = self.get_pair_data_for_features( pair, tf, strategy, corr_dataframes, base_dataframes, is_corr_pairs) informative_copy = informative_df.copy() for t in self.freqai_config["feature_parameters"]["indicator_periods_candles"]: df_features = strategy.feature_engineering_expand_all( - informative_copy.copy(), t) + informative_copy.copy(), t, metadata=metadata) suffix = f"{t}" informative_df = self.merge_features(informative_df, df_features, tf, tf, suffix) - generic_df = strategy.feature_engineering_expand_basic(informative_copy.copy()) + generic_df = strategy.feature_engineering_expand_basic( + informative_copy.copy(), metadata=metadata) suffix = "gen" informative_df = self.merge_features(informative_df, generic_df, tf, tf, suffix) @@ -1275,7 +1291,7 @@ class FreqaiDataKitchen: return dataframe - def use_strategy_to_populate_indicators( + def use_strategy_to_populate_indicators( # noqa: C901 self, strategy: IStrategy, corr_dataframes: dict = {}, @@ -1299,128 +1315,59 @@ class FreqaiDataKitchen: dataframe: DataFrame = dataframe containing populated indicators """ - # this is a hack to check if the user is using the populate_any_indicators function + # check if the user is using the deprecated populate_any_indicators function new_version = inspect.getsource(strategy.populate_any_indicators) == ( inspect.getsource(IStrategy.populate_any_indicators)) - if new_version: - tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes") - pairs: List[str] = self.freqai_config["feature_parameters"].get( - "include_corr_pairlist", []) + if not new_version: + raise OperationalException( + "You are using the `populate_any_indicators()` function" + " which was deprecated on March 1, 2023. Please refer " + "to the strategy migration guide to use the new " + "feature_engineering_* methods: \n" + "https://www.freqtrade.io/en/stable/strategy_migration/#freqai-strategy \n" + "And the feature_engineering_* documentation: \n" + "https://www.freqtrade.io/en/latest/freqai-feature-engineering/" + ) - for tf in tfs: - if tf not in base_dataframes: - base_dataframes[tf] = pd.DataFrame() - for p in pairs: - if p not in corr_dataframes: - corr_dataframes[p] = {} - if tf not in corr_dataframes[p]: - corr_dataframes[p][tf] = pd.DataFrame() - - if not prediction_dataframe.empty: - dataframe = prediction_dataframe.copy() - else: - dataframe = base_dataframes[self.config["timeframe"]].copy() - - corr_pairs: List[str] = self.freqai_config["feature_parameters"].get( - "include_corr_pairlist", []) - dataframe = self.populate_features(dataframe.copy(), pair, strategy, - corr_dataframes, base_dataframes) - - dataframe = strategy.feature_engineering_standard(dataframe.copy()) - # ensure corr pairs are always last - for corr_pair in corr_pairs: - if pair == corr_pair: - continue # dont repeat anything from whitelist - if corr_pairs and do_corr_pairs: - dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy, - corr_dataframes, base_dataframes, True) - - dataframe = strategy.set_freqai_targets(dataframe.copy()) - - self.get_unique_classes_from_labels(dataframe) - - dataframe = self.remove_special_chars_from_feature_names(dataframe) - - if self.config.get('reduce_df_footprint', False): - dataframe = reduce_dataframe_footprint(dataframe) - - return dataframe - - else: - # the user is using the populate_any_indicators functions which is deprecated - - df = self.use_strategy_to_populate_indicators_old_version( - strategy, corr_dataframes, base_dataframes, pair, - prediction_dataframe, do_corr_pairs) - return df - - def use_strategy_to_populate_indicators_old_version( - self, - strategy: IStrategy, - corr_dataframes: dict = {}, - base_dataframes: dict = {}, - pair: str = "", - prediction_dataframe: DataFrame = pd.DataFrame(), - do_corr_pairs: bool = True, - ) -> DataFrame: - """ - Use the user defined strategy for populating indicators during retrain - :param strategy: IStrategy = user defined strategy object - :param corr_dataframes: dict = dict containing the df pair dataframes - (for user defined timeframes) - :param base_dataframes: dict = dict containing the current pair dataframes - (for user defined timeframes) - :param metadata: dict = strategy furnished pair metadata - :return: - dataframe: DataFrame = dataframe containing populated indicators - """ - - # for prediction dataframe creation, we let dataprovider handle everything in the strategy - # so we create empty dictionaries, which allows us to pass None to - # `populate_any_indicators()`. Signaling we want the dp to give us the live dataframe. tfs: List[str] = self.freqai_config["feature_parameters"].get("include_timeframes") - pairs: List[str] = self.freqai_config["feature_parameters"].get("include_corr_pairlist", []) + pairs: List[str] = self.freqai_config["feature_parameters"].get( + "include_corr_pairlist", []) + + for tf in tfs: + if tf not in base_dataframes: + base_dataframes[tf] = pd.DataFrame() + for p in pairs: + if p not in corr_dataframes: + corr_dataframes[p] = {} + if tf not in corr_dataframes[p]: + corr_dataframes[p][tf] = pd.DataFrame() + if not prediction_dataframe.empty: dataframe = prediction_dataframe.copy() - for tf in tfs: - base_dataframes[tf] = None - for p in pairs: - if p not in corr_dataframes: - corr_dataframes[p] = {} - corr_dataframes[p][tf] = None else: dataframe = base_dataframes[self.config["timeframe"]].copy() - sgi = False - for tf in tfs: - if tf == tfs[-1]: - sgi = True # doing this last allows user to use all tf raw prices in labels - dataframe = strategy.populate_any_indicators( - pair, - dataframe.copy(), - tf, - informative=base_dataframes[tf], - set_generalized_indicators=sgi - ) - + corr_pairs: List[str] = self.freqai_config["feature_parameters"].get( + "include_corr_pairlist", []) + dataframe = self.populate_features(dataframe.copy(), pair, strategy, + corr_dataframes, base_dataframes) + metadata = {"pair": pair} + dataframe = strategy.feature_engineering_standard(dataframe.copy(), metadata=metadata) # ensure corr pairs are always last - for corr_pair in pairs: + for corr_pair in corr_pairs: if pair == corr_pair: continue # dont repeat anything from whitelist - for tf in tfs: - if pairs and do_corr_pairs: - dataframe = strategy.populate_any_indicators( - corr_pair, - dataframe.copy(), - tf, - informative=corr_dataframes[corr_pair][tf] - ) + if corr_pairs and do_corr_pairs: + dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy, + corr_dataframes, base_dataframes, True) + + if self.live: + dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata) + dataframe = self.remove_special_chars_from_feature_names(dataframe) self.get_unique_classes_from_labels(dataframe) - dataframe = self.remove_special_chars_from_feature_names(dataframe) - if self.config.get('reduce_df_footprint', False): dataframe = reduce_dataframe_footprint(dataframe) @@ -1546,3 +1493,25 @@ class FreqaiDataKitchen: dataframe.columns = dataframe.columns.str.replace(c, "") return dataframe + + def buffer_timerange(self, timerange: TimeRange): + """ + Buffer the start and end of the timerange. This is used *after* the indicators + are populated. + + The main example use is when predicting maxima and minima, the argrelextrema + function cannot know the maxima/minima at the edges of the timerange. To improve + model accuracy, it is best to compute argrelextrema on the full timerange + and then use this function to cut off the edges (buffer) by the kernel. + + In another case, if the targets are set to a shifted price movement, this + buffer is unnecessary because the shifted candles at the end of the timerange + will be NaN and FreqAI will automatically cut those off of the training + dataset. + """ + buffer = self.freqai_config["feature_parameters"]["buffer_train_data_candles"] + if buffer: + timerange.stopts -= buffer * timeframe_to_seconds(self.config["timeframe"]) + timerange.startts += buffer * timeframe_to_seconds(self.config["timeframe"]) + + return timerange diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 830970ba0..9cfda05ee 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -1,4 +1,3 @@ -import inspect import logging import threading import time @@ -22,7 +21,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_seconds from freqtrade.freqai.data_drawer import FreqaiDataDrawer from freqtrade.freqai.data_kitchen import FreqaiDataKitchen -from freqtrade.freqai.utils import plot_feature_importance, record_params +from freqtrade.freqai.utils import get_tb_logger, plot_feature_importance, record_params from freqtrade.strategy.interface import IStrategy @@ -66,12 +65,11 @@ class IFreqaiModel(ABC): self.retrain = False self.first = True self.set_full_path() - self.follow_mode: bool = self.freqai_info.get("follow_mode", False) self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", True) if self.save_backtest_models: logger.info('Backtesting module configured to save all models.') - self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode) + self.dd = FreqaiDataDrawer(Path(self.full_path), self.config) # set current candle to arbitrary historical date self.current_candle: datetime = datetime.fromtimestamp(637887600, tz=timezone.utc) self.dd.current_candle = self.current_candle @@ -82,9 +80,11 @@ class IFreqaiModel(ABC): if self.keras and self.ft_params.get("DI_threshold", 0): self.ft_params["DI_threshold"] = 0 logger.warning("DI threshold is not configured for Keras models yet. Deactivating.") + self.CONV_WIDTH = self.freqai_info.get('conv_width', 1) if self.ft_params.get("inlier_metric_window", 0): self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 + self.class_names: List[str] = [] # used in classification subclasses self.pair_it = 0 self.pair_it_train = 0 self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) @@ -106,8 +106,11 @@ class IFreqaiModel(ABC): self.data_provider: Optional[DataProvider] = None self.max_system_threads = max(int(psutil.cpu_count() * 2 - 2), 1) self.can_short = True # overridden in start() with strategy.can_short - - self.warned_deprecated_populate_any_indicators = False + self.model: Any = None + if self.ft_params.get('principal_component_analysis', False) and self.continual_learning: + self.ft_params.update({'principal_component_analysis': False}) + logger.warning('User tried to use PCA with continual learning. Deactivating PCA.') + self.activate_tensorboard: bool = self.freqai_info.get('activate_tensorboard', True) record_params(config, self.full_path) @@ -139,9 +142,6 @@ class IFreqaiModel(ABC): self.data_provider = strategy.dp self.can_short = strategy.can_short - # check if the strategy has deprecated populate_any_indicators function - self.check_deprecated_populate_any_indicators(strategy) - if self.live: self.inference_timer('start') self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"]) @@ -153,15 +153,14 @@ class IFreqaiModel(ABC): # (backtest window, i.e. window immediately following the training window). # FreqAI slides the window and sequentially builds the backtesting results before returning # the concatenated results for the full backtesting period back to the strategy. - elif not self.follow_mode: + else: self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"]) if not self.config.get("freqai_backtest_live_models", False): logger.info(f"Training {len(self.dk.training_timeranges)} timeranges") dk = self.start_backtesting(dataframe, metadata, self.dk, strategy) dataframe = dk.remove_features_from_df(dk.return_dataframe) else: - logger.info( - "Backtesting using historic predictions (live models)") + logger.info("Backtesting using historic predictions (live models)") dk = self.start_backtesting_from_historic_predictions( dataframe, metadata, self.dk) dataframe = dk.return_dataframe @@ -228,7 +227,7 @@ class IFreqaiModel(ABC): logger.warning(f'{pair} not in current whitelist, removing from train queue.') continue - (_, trained_timestamp, _) = self.dd.get_pair_dict_info(pair) + (_, trained_timestamp) = self.dd.get_pair_dict_info(pair) dk = FreqaiDataKitchen(self.config, self.live, pair) ( @@ -245,8 +244,8 @@ class IFreqaiModel(ABC): new_trained_timerange, pair, strategy, dk, data_load_timerange ) except Exception as msg: - logger.warning(f"Training {pair} raised exception {msg.__class__.__name__}. " - f"Message: {msg}, skipping.") + logger.exception(f"Training {pair} raised exception {msg.__class__.__name__}. " + f"Message: {msg}, skipping.") self.train_timer('stop', pair) @@ -286,7 +285,7 @@ class IFreqaiModel(ABC): # following tr_train. Both of these windows slide through the # entire backtest for tr_train, tr_backtest in zip(dk.training_timeranges, dk.backtesting_timeranges): - (_, _, _) = self.dd.get_pair_dict_info(pair) + (_, _) = self.dd.get_pair_dict_info(pair) train_it += 1 total_trains = len(dk.backtesting_timeranges) self.training_timerange = tr_train @@ -309,10 +308,11 @@ class IFreqaiModel(ABC): if dk.check_if_backtest_prediction_is_valid(len_backtest_df): if check_features: self.dd.load_metadata(dk) - dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators( - strategy, prediction_dataframe=dataframe.tail(1), pair=metadata["pair"] + df_fts = self.dk.use_strategy_to_populate_indicators( + strategy, prediction_dataframe=dataframe.tail(1), pair=pair ) - dk.find_features(dataframe_dummy_features) + df_fts = dk.remove_special_chars_from_feature_names(df_fts) + dk.find_features(df_fts) self.check_if_feature_list_matches_strategy(dk) check_features = False append_df = dk.get_backtesting_prediction() @@ -320,34 +320,46 @@ class IFreqaiModel(ABC): else: if populate_indicators: dataframe = self.dk.use_strategy_to_populate_indicators( - strategy, prediction_dataframe=dataframe, pair=metadata["pair"] + strategy, prediction_dataframe=dataframe, pair=pair ) populate_indicators = False dataframe_base_train = dataframe.loc[dataframe["date"] < tr_train.stopdt, :] - dataframe_base_train = strategy.set_freqai_targets(dataframe_base_train) + dataframe_base_train = strategy.set_freqai_targets( + dataframe_base_train, metadata=metadata) dataframe_base_backtest = dataframe.loc[dataframe["date"] < tr_backtest.stopdt, :] - dataframe_base_backtest = strategy.set_freqai_targets(dataframe_base_backtest) + dataframe_base_backtest = strategy.set_freqai_targets( + dataframe_base_backtest, metadata=metadata) + + tr_train = dk.buffer_timerange(tr_train) dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train) dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest) + dataframe_train = dk.remove_special_chars_from_feature_names(dataframe_train) + dataframe_backtest = dk.remove_special_chars_from_feature_names(dataframe_backtest) + dk.get_unique_classes_from_labels(dataframe_train) + if not self.model_exists(dk): dk.find_features(dataframe_train) dk.find_labels(dataframe_train) try: + self.tb_logger = get_tb_logger(self.dd.model_type, dk.data_path, + self.activate_tensorboard) self.model = self.train(dataframe_train, pair, dk) + self.tb_logger.close() except Exception as msg: logger.warning( f"Training {pair} raised exception {msg.__class__.__name__}. " - f"Message: {msg}, skipping.") + f"Message: {msg}, skipping.", exc_info=True) + self.model = None self.dd.pair_dict[pair]["trained_timestamp"] = int( tr_train.stopts) - if self.plot_features: + if self.plot_features and self.model is not None: plot_feature_importance(self.model, pair, dk, self.plot_features) - if self.save_backtest_models: + if self.save_backtest_models and self.model is not None: logger.info('Saving backtest model to disk.') self.dd.save_data(self.model, pair, dk) else: @@ -379,18 +391,9 @@ class IFreqaiModel(ABC): :returns: dk: FreqaiDataKitchen = Data management/analysis tool associated to present pair only """ - # update follower - if self.follow_mode: - self.dd.update_follower_metadata() # get the model metadata associated with the current pair - (_, trained_timestamp, return_null_array) = self.dd.get_pair_dict_info(metadata["pair"]) - - # if the metadata doesn't exist, the follower returns null arrays to strategy - if self.follow_mode and return_null_array: - logger.info("Returning null array from follower to strategy") - self.dd.return_null_values_to_strategy(dataframe, dk) - return dk + (_, trained_timestamp) = self.dd.get_pair_dict_info(metadata["pair"]) # append the historic data once per round if self.dd.historic_data: @@ -398,27 +401,18 @@ class IFreqaiModel(ABC): logger.debug(f'Updating historic data on pair {metadata["pair"]}') self.track_current_candle() - if not self.follow_mode: + (_, new_trained_timerange, data_load_timerange) = dk.check_if_new_training_required( + trained_timestamp + ) + dk.set_paths(metadata["pair"], new_trained_timerange.stopts) - (_, new_trained_timerange, data_load_timerange) = dk.check_if_new_training_required( - trained_timestamp - ) - dk.set_paths(metadata["pair"], new_trained_timerange.stopts) + # load candle history into memory if it is not yet. + if not self.dd.historic_data: + self.dd.load_all_pair_histories(data_load_timerange, dk) - # load candle history into memory if it is not yet. - if not self.dd.historic_data: - self.dd.load_all_pair_histories(data_load_timerange, dk) - - if not self.scanning: - self.scanning = True - self.start_scanning(strategy) - - elif self.follow_mode: - dk.set_paths(metadata["pair"], trained_timestamp) - logger.info( - "FreqAI instance set to follow_mode, finding existing pair " - f"using { self.identifier }" - ) + if not self.scanning: + self.scanning = True + self.start_scanning(strategy) # load the model and associated data into the data kitchen self.model = self.dd.load_data(metadata["pair"], dk) @@ -501,12 +495,12 @@ class IFreqaiModel(ABC): if dk.training_features_list != feature_list: raise OperationalException( "Trying to access pretrained model with `identifier` " - "but found different features furnished by current strategy." - "Change `identifier` to train from scratch, or ensure the" - "strategy is furnishing the same features as the pretrained" + "but found different features furnished by current strategy. " + "Change `identifier` to train from scratch, or ensure the " + "strategy is furnishing the same features as the pretrained " "model. In case of --strategy-list, please be aware that FreqAI " "requires all strategies to maintain identical " - "populate_any_indicator() functions" + "feature_engineering_* functions" ) def data_cleaning_train(self, dk: FreqaiDataKitchen) -> None: @@ -580,7 +574,14 @@ class IFreqaiModel(ABC): :return: :boolean: whether the model file exists or not. """ - path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model.joblib") + if self.dd.model_type == 'joblib': + file_type = ".joblib" + elif self.dd.model_type == 'keras': + file_type = ".h5" + elif self.dd.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: + file_type = ".zip" + + path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") file_exists = path_to_modelfile.is_file() if file_exists: logger.info("Found model at %s", dk.data_path / dk.model_filename) @@ -612,7 +613,7 @@ class IFreqaiModel(ABC): :param strategy: IStrategy = user defined strategy object :param dk: FreqaiDataKitchen = non-persistent data container for current coin/loop :param data_load_timerange: TimeRange = the amount of data to be loaded - for populate_any_indicators + for populating indicators (larger than new_trained_timerange so that new_trained_timerange does not contain any NaNs) """ @@ -625,23 +626,29 @@ class IFreqaiModel(ABC): strategy, corr_dataframes, base_dataframes, pair ) - unfiltered_dataframe = dk.slice_dataframe(new_trained_timerange, unfiltered_dataframe) + trained_timestamp = new_trained_timerange.stopts + + buffered_timerange = dk.buffer_timerange(new_trained_timerange) + + unfiltered_dataframe = dk.slice_dataframe(buffered_timerange, unfiltered_dataframe) # find the features indicated by strategy and store in datakitchen dk.find_features(unfiltered_dataframe) dk.find_labels(unfiltered_dataframe) + self.tb_logger = get_tb_logger(self.dd.model_type, dk.data_path, + self.activate_tensorboard) model = self.train(unfiltered_dataframe, pair, dk) + self.tb_logger.close() - self.dd.pair_dict[pair]["trained_timestamp"] = new_trained_timerange.stopts - dk.set_new_model_names(pair, new_trained_timerange.stopts) + self.dd.pair_dict[pair]["trained_timestamp"] = trained_timestamp + dk.set_new_model_names(pair, trained_timestamp) self.dd.save_data(model, pair, dk) if self.plot_features: plot_feature_importance(model, pair, dk, self.plot_features) - if self.freqai_info.get("purge_old_models", False): - self.dd.purge_old_models() + self.dd.purge_old_models() def set_initial_historic_predictions( self, pred_df: DataFrame, dk: FreqaiDataKitchen, pair: str, strat_df: DataFrame @@ -817,7 +824,7 @@ class IFreqaiModel(ABC): logger.warning("Couldn't cache corr_pair dataframes for improved performance. " "Consider ensuring that the full coin/stake, e.g. XYZ/USD, " "is included in the column names when you are creating features " - "in `populate_any_indicators()`.") + "in `feature_engineering_*` functions.") self.get_corr_dataframes = not bool(self.corr_dataframes) elif self.corr_dataframes: dataframe = dk.attach_corr_pair_columns( @@ -944,26 +951,6 @@ class IFreqaiModel(ABC): dk.return_dataframe, saved_dataframe, how='left', left_on='date', right_on="date_pred") return dk - def check_deprecated_populate_any_indicators(self, strategy: IStrategy): - """ - Check and warn if the deprecated populate_any_indicators function is used. - :param strategy: strategy object - """ - - if not self.warned_deprecated_populate_any_indicators: - self.warned_deprecated_populate_any_indicators = True - old_version = inspect.getsource(strategy.populate_any_indicators) != ( - inspect.getsource(IStrategy.populate_any_indicators)) - - if old_version: - logger.warning("DEPRECATION WARNING: " - "You are using the deprecated populate_any_indicators function. " - "This function will raise an error on March 1 2023. " - "Please update your strategy by using " - "the new feature_engineering functions. See \n" - "https://www.freqtrade.io/en/latest/freqai-feature-engineering/" - "for details.") - # Following methods which are overridden by user made prediction models. # See freqai/prediction_models/CatboostPredictionModel.py for an example. diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index ca1d8ece0..b9904e40d 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -14,16 +14,20 @@ logger = logging.getLogger(__name__) class CatboostClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ train_data = Pool( diff --git a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py index c6f900fad..58c47566a 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py @@ -15,16 +15,20 @@ logger = logging.getLogger(__name__) class CatboostClassifierMultiTarget(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ cbc = CatBoostClassifier( diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index 4b17a703b..28b1b11cc 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -14,16 +14,20 @@ logger = logging.getLogger(__name__) class CatboostRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ train_data = Pool( diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index 976d0b29b..1562c2024 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -15,16 +15,20 @@ logger = logging.getLogger(__name__) class CatboostRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ cbr = CatBoostRegressor( diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index e467ad3c1..45f3a31d0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class LightGBMClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0: diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py index d1eb6daa2..72a8ee259 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class LightGBMClassifierMultiTarget(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ lgb = LGBMClassifier(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 85c9b691c..3d1c30ed3 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -12,18 +12,20 @@ logger = logging.getLogger(__name__) class LightGBMRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ - Most regressors use the same function names and arguments e.g. user - can drop in LGBMRegressor in place of CatBoostRegressor and all data - management will be properly handled by Freqai. - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0: diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 37c6bb186..663a611f0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class LightGBMRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ lgb = LGBMRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py new file mode 100644 index 000000000..71279dba9 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -0,0 +1,91 @@ +from typing import Any, Dict + +import torch + +from freqtrade.freqai.base_models.BasePyTorchClassifier import BasePyTorchClassifier +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) +from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer + + +class PyTorchMLPClassifier(BasePyTorchClassifier): + """ + This class implements the fit method of IFreqaiModel. + in the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchClassifier + predict method that expects the model to predict a tensor of type long. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": null, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor( + target_tensor_type=torch.long, + squeeze_target_tensor=True + ) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + :raises ValueError: If self.class_names is not defined in the parent class. + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=len(class_names), + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + # check if continual_learning is activated, and retreive the model to continue training + trainer = self.get_init_model(dk.pair) + if trainer is None: + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + data_convertor=self.data_convertor, + tb_logger=self.tb_logger, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py new file mode 100644 index 000000000..9f4534487 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -0,0 +1,85 @@ +from typing import Any, Dict + +import torch + +from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) +from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer + + +class PyTorchMLPRegressor(BasePyTorchRegressor): + """ + This class implements the fit method of IFreqaiModel. + in the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchRegressor + predict method that expects the model to predict tensor of type float. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": null, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + """ + + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + # check if continual_learning is activated, and retreive the model to continue training + trainer = self.get_init_model(dk.pair) + if trainer is None: + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + data_convertor=self.data_convertor, + tb_logger=self.tb_logger, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py new file mode 100644 index 000000000..b3b684c14 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py @@ -0,0 +1,140 @@ +from typing import Any, Dict, Tuple + +import numpy as np +import numpy.typing as npt +import pandas as pd +import torch + +from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchTransformerTrainer +from freqtrade.freqai.torch.PyTorchTransformerModel import PyTorchTransformerModel + + +class PyTorchTransformerRegressor(BasePyTorchRegressor): + """ + This class implements the fit method of IFreqaiModel. + in the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchRegressor + predict method that expects the model to predict tensor of type float. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": null + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + """ + + n_features = data_dictionary["train_features"].shape[-1] + n_labels = data_dictionary["train_labels"].shape[-1] + model = PyTorchTransformerModel( + input_dim=n_features, + output_dim=n_labels, + time_window=self.window_size, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + # check if continual_learning is activated, and retreive the model to continue training + trainer = self.get_init_model(dk.pair) + if trainer is None: + trainer = PyTorchTransformerTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + data_convertor=self.data_convertor, + window_size=self.window_size, + tb_logger=self.tb_logger, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer + + def predict( + self, unfiltered_df: pd.DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[pd.DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + """ + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + + self.data_cleaning_predict(dk) + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) + # if user is asking for multiple predictions, slide the window + # along the tensor + x = x.unsqueeze(0) + # create empty torch tensor + self.model.model.eval() + yb = torch.empty(0).to(self.device) + if x.shape[1] > 1: + ws = self.window_size + for i in range(0, x.shape[1] - ws): + xb = x[:, i:i + ws, :].to(self.device) + y = self.model.model(xb) + yb = torch.cat((yb, y), dim=0) + else: + yb = self.model.model(x) + + yb = yb.cpu().squeeze() + pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list) + pred_df = dk.denormalize_labels_from_metadata(pred_df) + + if x.shape[1] > 1: + zeros_df = pd.DataFrame(np.zeros((x.shape[1] - len(pred_df), len(pred_df.columns))), + columns=pred_df.columns) + pred_df = pd.concat([zeros_df, pred_df], axis=0, ignore_index=True) + return (pred_df, dk.do_predict) diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index 2a87151f9..a11decc92 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -1,11 +1,12 @@ import logging from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Type import torch as th from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.RL.Base5ActionRLEnv import Actions, Base5ActionRLEnv, Positions +from freqtrade.freqai.RL.BaseEnvironment import BaseEnvironment from freqtrade.freqai.RL.BaseReinforcementLearningModel import BaseReinforcementLearningModel @@ -57,10 +58,14 @@ class ReinforcementLearner(BaseReinforcementLearningModel): policy_kwargs = dict(activation_fn=th.nn.ReLU, net_arch=self.net_arch) + if self.activate_tensorboard: + tb_path = Path(dk.full_path / "tensorboard" / dk.pair.split('/')[0]) + else: + tb_path = None + if dk.pair not in self.dd.model_dictionary or not self.continual_learning: model = self.MODELCLASS(self.policy_type, self.train_env, policy_kwargs=policy_kwargs, - tensorboard_log=Path( - dk.full_path / "tensorboard" / dk.pair.split('/')[0]), + tensorboard_log=tb_path, **self.freqai_info.get('model_training_parameters', {}) ) else: @@ -71,7 +76,8 @@ class ReinforcementLearner(BaseReinforcementLearningModel): model.learn( total_timesteps=int(total_timesteps), - callback=[self.eval_callback, self.tensorboard_callback] + callback=[self.eval_callback, self.tensorboard_callback], + progress_bar=self.rl_config.get('progress_bar', False) ) if Path(dk.data_path / "best_model.zip").is_file(): @@ -83,7 +89,9 @@ class ReinforcementLearner(BaseReinforcementLearningModel): return model - class MyRLEnv(Base5ActionRLEnv): + MyRLEnv: Type[BaseEnvironment] + + class MyRLEnv(Base5ActionRLEnv): # type: ignore[no-redef] """ User can override any function in BaseRLEnv and gym.Env. Here the user sets a custom reward based on profit and trade duration. @@ -93,6 +101,12 @@ class ReinforcementLearner(BaseReinforcementLearningModel): """ An example reward function. This is the one function that users will likely wish to inject their own creativity into. + + Warning! + This is function is a showcase of functionality designed to show as many possible + environment control features as possible. It is also designed to run quickly + on small computers. This is a benchmark, it is *not* for live production. + :param action: int = The action made by the agent for the current candle. :return: float = the reward to give to the agent for current step (used for optimization @@ -100,7 +114,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel): """ # first, penalize if the action is not valid if not self._is_valid(action): - self.tensorboard_log("is_valid") + self.tensorboard_log("invalid", category="actions") return -2 pnl = self.get_unrealized_profit() diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py index a9be87b0b..9f0b2d436 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py @@ -3,12 +3,12 @@ from typing import Any, Dict from pandas import DataFrame from stable_baselines3.common.callbacks import EvalCallback -from stable_baselines3.common.vec_env import SubprocVecEnv +from stable_baselines3.common.vec_env import SubprocVecEnv, VecMonitor from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner from freqtrade.freqai.RL.BaseReinforcementLearningModel import make_env -from freqtrade.freqai.RL.TensorboardCallback import TensorboardCallback +from freqtrade.freqai.tensorboard.TensorboardCallback import TensorboardCallback logger = logging.getLogger(__name__) @@ -34,24 +34,32 @@ class ReinforcementLearner_multiproc(ReinforcementLearner): train_df = data_dictionary["train_features"] test_df = data_dictionary["test_features"] - env_info = self.pack_env_dict() + if self.train_env: + self.train_env.close() + if self.eval_env: + self.eval_env.close() + + env_info = self.pack_env_dict(dk.pair) + + eval_freq = len(train_df) // self.max_threads env_id = "train_env" - self.train_env = SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, - train_df, prices_train, - monitor=True, - env_info=env_info) for i - in range(self.max_threads)]) + self.train_env = VecMonitor(SubprocVecEnv([make_env(self.MyRLEnv, env_id, i, 1, + train_df, prices_train, + env_info=env_info) for i + in range(self.max_threads)])) eval_env_id = 'eval_env' - self.eval_env = SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1, - test_df, prices_test, - monitor=True, - env_info=env_info) for i - in range(self.max_threads)]) + self.eval_env = VecMonitor(SubprocVecEnv([make_env(self.MyRLEnv, eval_env_id, i, 1, + test_df, prices_test, + env_info=env_info) for i + in range(self.max_threads)])) + self.eval_callback = EvalCallback(self.eval_env, deterministic=True, - render=False, eval_freq=len(train_df), + render=False, eval_freq=eval_freq, best_model_save_path=str(dk.data_path)) + # TENSORBOARD CALLBACK DOES NOT RECOMMENDED TO USE WITH MULTIPLE ENVS, + # IT WILL RETURN FALSE INFORMATIONS, NEVERTHLESS NOT THREAD SAFE WITH SB3!!! actions = self.train_env.env_method("get_actions")[0] self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions) diff --git a/freqtrade/freqai/prediction_models/XGBoostClassifier.py b/freqtrade/freqai/prediction_models/XGBoostClassifier.py index 67c7c7783..b6f04b497 100644 --- a/freqtrade/freqai/prediction_models/XGBoostClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostClassifier.py @@ -18,16 +18,20 @@ logger = logging.getLogger(__name__) class XGBoostClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"].to_numpy() diff --git a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py index 470c283ea..20156e9fd 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py @@ -18,16 +18,20 @@ logger = logging.getLogger(__name__) class XGBoostRFClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"].to_numpy() diff --git a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py index e7cc27f2e..1aefbf19a 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class XGBoostRFRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"] diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index 9a280286b..f8b4d353d 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -5,6 +5,7 @@ from xgboost import XGBRegressor from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.tensorboard import TBCallback logger = logging.getLogger(__name__) @@ -12,16 +13,20 @@ logger = logging.getLogger(__name__) class XGBoostRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"] @@ -40,7 +45,10 @@ class XGBoostRegressor(BaseRegressionModel): model = XGBRegressor(**self.model_training_parameters) + model.set_params(callbacks=[TBCallback(dk.data_path)], activate=self.activate_tensorboard) model.fit(X=X, y=y, sample_weight=sample_weight, eval_set=eval_set, sample_weight_eval_set=eval_weights, xgb_model=xgb_model) + # set the callbacks to empty so that we can serialize to disk later + model.set_params(callbacks=[]) return model diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py index 920745ec9..a0330485e 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class XGBoostRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ xgb = XGBRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/RL/TensorboardCallback.py b/freqtrade/freqai/tensorboard/TensorboardCallback.py similarity index 67% rename from freqtrade/freqai/RL/TensorboardCallback.py rename to freqtrade/freqai/tensorboard/TensorboardCallback.py index b596742e9..61652c9c6 100644 --- a/freqtrade/freqai/RL/TensorboardCallback.py +++ b/freqtrade/freqai/tensorboard/TensorboardCallback.py @@ -3,8 +3,9 @@ from typing import Any, Dict, Type, Union from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.logger import HParam +from stable_baselines3.common.vec_env import VecEnv -from freqtrade.freqai.RL.BaseEnvironment import BaseActions, BaseEnvironment +from freqtrade.freqai.RL.BaseEnvironment import BaseActions class TensorboardCallback(BaseCallback): @@ -12,11 +13,13 @@ class TensorboardCallback(BaseCallback): Custom callback for plotting additional values in tensorboard and episodic summary reports. """ + # Override training_env type to fix type errors + training_env: Union[VecEnv, None] = None + def __init__(self, verbose=1, actions: Type[Enum] = BaseActions): - super(TensorboardCallback, self).__init__(verbose) + super().__init__(verbose) self.model: Any = None - self.logger = None # type: Any - self.training_env: BaseEnvironment = None # type: ignore + self.logger: Any = None self.actions: Type[Enum] = actions def _on_training_start(self) -> None: @@ -44,16 +47,16 @@ class TensorboardCallback(BaseCallback): def _on_step(self) -> bool: local_info = self.locals["infos"][0] + if self.training_env is None: + return True tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] - for info in local_info: - if info not in ["episode", "terminal_observation"]: - self.logger.record(f"_info/{info}", local_info[info]) + for metric in local_info: + if metric not in ["episode", "terminal_observation"]: + self.logger.record(f"info/{metric}", local_info[metric]) - for info in tensorboard_metrics: - if info in [action.name for action in self.actions]: - self.logger.record(f"_actions/{info}", tensorboard_metrics[info]) - else: - self.logger.record(f"_custom/{info}", tensorboard_metrics[info]) + for category in tensorboard_metrics: + for metric in tensorboard_metrics[category]: + self.logger.record(f"{category}/{metric}", tensorboard_metrics[category][metric]) return True diff --git a/freqtrade/freqai/tensorboard/__init__.py b/freqtrade/freqai/tensorboard/__init__.py new file mode 100644 index 000000000..59862bc0d --- /dev/null +++ b/freqtrade/freqai/tensorboard/__init__.py @@ -0,0 +1,15 @@ +# ensure users can still use a non-torch freqai version +try: + from freqtrade.freqai.tensorboard.tensorboard import TensorBoardCallback, TensorboardLogger + TBLogger = TensorboardLogger + TBCallback = TensorBoardCallback +except ModuleNotFoundError: + from freqtrade.freqai.tensorboard.base_tensorboard import (BaseTensorBoardCallback, + BaseTensorboardLogger) + TBLogger = BaseTensorboardLogger # type: ignore + TBCallback = BaseTensorBoardCallback # type: ignore + +__all__ = ( + "TBLogger", + "TBCallback" +) diff --git a/freqtrade/freqai/tensorboard/base_tensorboard.py b/freqtrade/freqai/tensorboard/base_tensorboard.py new file mode 100644 index 000000000..c2d47137e --- /dev/null +++ b/freqtrade/freqai/tensorboard/base_tensorboard.py @@ -0,0 +1,35 @@ +import logging +from pathlib import Path +from typing import Any + +from xgboost.callback import TrainingCallback + + +logger = logging.getLogger(__name__) + + +class BaseTensorboardLogger: + def __init__(self, logdir: Path, activate: bool = True): + logger.warning("Tensorboard is not installed, no logs will be written." + "Ensure torch is installed, or use the torch/RL docker images") + + def log_scalar(self, tag: str, scalar_value: Any, step: int): + return + + def close(self): + return + + +class BaseTensorBoardCallback(TrainingCallback): + + def __init__(self, logdir: Path, activate: bool = True): + logger.warning("Tensorboard is not installed, no logs will be written." + "Ensure torch is installed, or use the torch/RL docker images") + + def after_iteration( + self, model, epoch: int, evals_log: TrainingCallback.EvalsLog + ) -> bool: + return False + + def after_training(self, model): + return model diff --git a/freqtrade/freqai/tensorboard/tensorboard.py b/freqtrade/freqai/tensorboard/tensorboard.py new file mode 100644 index 000000000..46bf8dc61 --- /dev/null +++ b/freqtrade/freqai/tensorboard/tensorboard.py @@ -0,0 +1,62 @@ +import logging +from pathlib import Path +from typing import Any + +from torch.utils.tensorboard import SummaryWriter +from xgboost import callback + +from freqtrade.freqai.tensorboard.base_tensorboard import (BaseTensorBoardCallback, + BaseTensorboardLogger) + + +logger = logging.getLogger(__name__) + + +class TensorboardLogger(BaseTensorboardLogger): + def __init__(self, logdir: Path, activate: bool = True): + self.activate = activate + if self.activate: + self.writer: SummaryWriter = SummaryWriter(f"{str(logdir)}/tensorboard") + + def log_scalar(self, tag: str, scalar_value: Any, step: int): + if self.activate: + self.writer.add_scalar(tag, scalar_value, step) + + def close(self): + if self.activate: + self.writer.flush() + self.writer.close() + + +class TensorBoardCallback(BaseTensorBoardCallback): + + def __init__(self, logdir: Path, activate: bool = True): + self.activate = activate + if self.activate: + self.writer: SummaryWriter = SummaryWriter(f"{str(logdir)}/tensorboard") + + def after_iteration( + self, model, epoch: int, evals_log: callback.TrainingCallback.EvalsLog + ) -> bool: + if not self.activate: + return False + if not evals_log: + return False + + for data, metric in evals_log.items(): + for metric_name, log in metric.items(): + score = log[-1][0] if isinstance(log[-1], tuple) else log[-1] + if data == "train": + self.writer.add_scalar("train_loss", score, epoch) + else: + self.writer.add_scalar("valid_loss", score, epoch) + + return False + + def after_training(self, model): + if not self.activate: + return model + self.writer.flush() + self.writer.close() + + return model diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py new file mode 100644 index 000000000..e6b815373 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -0,0 +1,67 @@ +from abc import ABC, abstractmethod +from typing import Optional + +import pandas as pd +import torch + + +class PyTorchDataConvertor(ABC): + """ + This class is responsible for converting `*_features` & `*_labels` pandas dataframes + to pytorch tensors. + """ + + @abstractmethod + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: + """ + :param df: "*_features" dataframe. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). + """ + + @abstractmethod + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: + """ + :param df: "*_labels" dataframe. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). + """ + + +class DefaultPyTorchDataConvertor(PyTorchDataConvertor): + """ + A default conversion that keeps features dataframe shapes. + """ + + def __init__( + self, + target_tensor_type: Optional[torch.dtype] = None, + squeeze_target_tensor: bool = False + ): + """ + :param target_tensor_type: type of target tensor, for classification use + torch.long, for regressor use torch.float or torch.double. + :param squeeze_target_tensor: controls the target shape, used for loss functions + that requires 0D or 1D. + """ + self._target_tensor_type = target_tensor_type + self._squeeze_target_tensor = squeeze_target_tensor + + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: + x = torch.from_numpy(df.values).float() + if device: + x = x.to(device) + + return x + + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> torch.Tensor: + y = torch.from_numpy(df.values) + + if self._target_tensor_type: + y = y.to(self._target_tensor_type) + + if self._squeeze_target_tensor: + y = y.squeeze() + + if device: + y = y.to(device) + + return y diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py new file mode 100644 index 000000000..0093388f8 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -0,0 +1,96 @@ +import logging + +import torch +from torch import nn + + +logger = logging.getLogger(__name__) + + +class PyTorchMLPModel(nn.Module): + """ + A multi-layer perceptron (MLP) model implemented using PyTorch. + + This class mainly serves as a simple example for the integration of PyTorch model's + to freqai. It is not optimized at all and should not be used for production purposes. + + :param input_dim: The number of input features. This parameter specifies the number + of features in the input data that the MLP will use to make predictions. + :param output_dim: The number of output classes. This parameter specifies the number + of classes that the MLP will predict. + :param hidden_dim: The number of hidden units in each layer. This parameter controls + the complexity of the MLP and determines how many nonlinear relationships the MLP + can represent. Increasing the number of hidden units can increase the capacity of + the MLP to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 256 + :param dropout_percent: The dropout rate for regularization. This parameter specifies + the probability of dropping out a neuron during training to prevent overfitting. + The dropout rate should be tuned carefully to balance between underfitting and + overfitting. Default: 0.2 + :param n_layer: The number of layers in the MLP. This parameter specifies the number + of layers in the MLP architecture. Adding more layers to the MLP can increase its + capacity to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 1 + + :returns: The output of the MLP, with shape (batch_size, output_dim) + """ + + def __init__(self, input_dim: int, output_dim: int, **kwargs): + super().__init__() + hidden_dim: int = kwargs.get("hidden_dim", 256) + dropout_percent: int = kwargs.get("dropout_percent", 0.2) + n_layer: int = kwargs.get("n_layer", 1) + self.input_layer = nn.Linear(input_dim, hidden_dim) + self.blocks = nn.Sequential(*[Block(hidden_dim, dropout_percent) for _ in range(n_layer)]) + self.output_layer = nn.Linear(hidden_dim, output_dim) + self.relu = nn.ReLU() + self.dropout = nn.Dropout(p=dropout_percent) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # x: torch.Tensor = tensors[0] + x = self.relu(self.input_layer(x)) + x = self.dropout(x) + x = self.blocks(x) + x = self.output_layer(x) + return x + + +class Block(nn.Module): + """ + A building block for a multi-layer perceptron (MLP). + + :param hidden_dim: The number of hidden units in the feedforward network. + :param dropout_percent: The dropout rate for regularization. + + :returns: torch.Tensor. with shape (batch_size, hidden_dim) + """ + + def __init__(self, hidden_dim: int, dropout_percent: int): + super().__init__() + self.ff = FeedForward(hidden_dim) + self.dropout = nn.Dropout(p=dropout_percent) + self.ln = nn.LayerNorm(hidden_dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.ff(self.ln(x)) + x = self.dropout(x) + return x + + +class FeedForward(nn.Module): + """ + A simple fully-connected feedforward neural network block. + + :param hidden_dim: The number of hidden units in the block. + :return: torch.Tensor. with shape (batch_size, hidden_dim) + """ + + def __init__(self, hidden_dim: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU(), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.net(x) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py new file mode 100644 index 000000000..603e7ac12 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -0,0 +1,229 @@ +import logging +import math +from pathlib import Path +from typing import Any, Dict, List, Optional + +import pandas as pd +import torch +from torch import nn +from torch.optim import Optimizer +from torch.utils.data import DataLoader, TensorDataset + +from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchTrainerInterface import PyTorchTrainerInterface + +from .datasets import WindowDataset + + +logger = logging.getLogger(__name__) + + +class PyTorchModelTrainer(PyTorchTrainerInterface): + def __init__( + self, + model: nn.Module, + optimizer: Optimizer, + criterion: nn.Module, + device: str, + data_convertor: PyTorchDataConvertor, + model_meta_data: Dict[str, Any] = {}, + window_size: int = 1, + tb_logger: Any = None, + **kwargs + ): + """ + :param model: The PyTorch model to be trained. + :param optimizer: The optimizer to use for training. + :param criterion: The loss function to use for training. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). + :param init_model: A dictionary containing the initial model/optimizer + state_dict and model_meta_data saved by self.save() method. + :param model_meta_data: Additional metadata about the model (optional). + :param data_convertor: convertor from pd.DataFrame to torch.tensor. + :param max_iters: The number of training iterations to run. + iteration here refers to the number of times we call + self.optimizer.step(). used to calculate n_epochs. + :param batch_size: The size of the batches to use during training. + :param max_n_eval_batches: The maximum number batches to use for evaluation. + """ + self.model = model + self.optimizer = optimizer + self.criterion = criterion + self.model_meta_data = model_meta_data + self.device = device + self.max_iters: int = kwargs.get("max_iters", 100) + self.batch_size: int = kwargs.get("batch_size", 64) + self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) + self.data_convertor = data_convertor + self.window_size: int = window_size + self.tb_logger = tb_logger + + def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]): + """ + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :param splits: splits to use in training, splits must contain "train", + optional "test" could be added by setting freqai.data_split_parameters.test_size > 0 + in the config file. + + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. + """ + data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits) + epochs = self.calc_n_epochs( + n_obs=len(data_dictionary["train_features"]), + batch_size=self.batch_size, + n_iters=self.max_iters + ) + self.model.train() + for epoch in range(1, epochs + 1): + for i, batch_data in enumerate(data_loaders_dictionary["train"]): + + xb, yb = batch_data + xb.to(self.device) + yb.to(self.device) + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + + self.optimizer.zero_grad(set_to_none=True) + loss.backward() + self.optimizer.step() + self.tb_logger.log_scalar("train_loss", loss.item(), i) + + # evaluation + if "test" in splits: + self.estimate_loss( + data_loaders_dictionary, + self.max_n_eval_batches, + "test" + ) + + @torch.no_grad() + def estimate_loss( + self, + data_loader_dictionary: Dict[str, DataLoader], + max_n_eval_batches: Optional[int], + split: str, + ) -> None: + self.model.eval() + n_batches = 0 + for i, batch_data in enumerate(data_loader_dictionary[split]): + if max_n_eval_batches and i > max_n_eval_batches: + n_batches += 1 + break + xb, yb = batch_data + xb.to(self.device) + yb.to(self.device) + + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + self.tb_logger.log_scalar(f"{split}_loss", loss.item(), i) + + self.model.train() + + def create_data_loaders_dictionary( + self, + data_dictionary: Dict[str, pd.DataFrame], + splits: List[str] + ) -> Dict[str, DataLoader]: + """ + Converts the input data to PyTorch tensors using a data loader. + """ + data_loader_dictionary = {} + for split in splits: + x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"], self.device) + y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"], self.device) + dataset = TensorDataset(x, y) + data_loader = DataLoader( + dataset, + batch_size=self.batch_size, + shuffle=True, + drop_last=True, + num_workers=0, + ) + data_loader_dictionary[split] = data_loader + + return data_loader_dictionary + + @staticmethod + def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int: + """ + Calculates the number of epochs required to reach the maximum number + of iterations specified in the model training parameters. + + the motivation here is that `max_iters` is easier to optimize and keep stable, + across different n_obs - the number of data points. + """ + + n_batches = math.ceil(n_obs // batch_size) + epochs = math.ceil(n_iters // n_batches) + if epochs <= 10: + logger.warning("User set `max_iters` in such a way that the trainer will only perform " + f" {epochs} epochs. Please consider increasing this value accordingly") + if epochs <= 1: + logger.warning("Epochs set to 1. Please review your `max_iters` value") + epochs = 1 + return epochs + + def save(self, path: Path): + """ + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + """ + + torch.save({ + "model_state_dict": self.model.state_dict(), + "optimizer_state_dict": self.optimizer.state_dict(), + "model_meta_data": self.model_meta_data, + "pytrainer": self + }, path) + + def load(self, path: Path): + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + def load_from_checkpoint(self, checkpoint: Dict): + """ + when using continual_learning, DataDrawer will load the dictionary + (containing state dicts and model_meta_data) by calling torch.load(path). + you can access this dict from any class that inherits IFreqaiModel by calling + get_init_model method. + """ + self.model.load_state_dict(checkpoint["model_state_dict"]) + self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) + self.model_meta_data = checkpoint["model_meta_data"] + return self + + +class PyTorchTransformerTrainer(PyTorchModelTrainer): + """ + Creating a trainer for the Transformer model. + """ + + def create_data_loaders_dictionary( + self, + data_dictionary: Dict[str, pd.DataFrame], + splits: List[str] + ) -> Dict[str, DataLoader]: + """ + Converts the input data to PyTorch tensors using a data loader. + """ + data_loader_dictionary = {} + for split in splits: + x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"], self.device) + y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"], self.device) + dataset = WindowDataset(x, y, self.window_size) + data_loader = DataLoader( + dataset, + batch_size=self.batch_size, + shuffle=False, + drop_last=True, + num_workers=0, + ) + data_loader_dictionary[split] = data_loader + + return data_loader_dictionary diff --git a/freqtrade/freqai/torch/PyTorchTrainerInterface.py b/freqtrade/freqai/torch/PyTorchTrainerInterface.py new file mode 100644 index 000000000..840c145f7 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchTrainerInterface.py @@ -0,0 +1,53 @@ +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Dict, List + +import pandas as pd +import torch +from torch import nn + + +class PyTorchTrainerInterface(ABC): + + @abstractmethod + def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]) -> None: + """ + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :param splits: splits to use in training, splits must contain "train", + optional "test" could be added by setting freqai.data_split_parameters.test_size > 0 + in the config file. + + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. + """ + + @abstractmethod + def save(self, path: Path) -> None: + """ + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + """ + + def load(self, path: Path) -> nn.Module: + """ + :param path: path to zip file. + :returns: pytorch model. + """ + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + @abstractmethod + def load_from_checkpoint(self, checkpoint: Dict) -> nn.Module: + """ + when using continual_learning, DataDrawer will load the dictionary + (containing state dicts and model_meta_data) by calling torch.load(path). + you can access this dict from any class that inherits IFreqaiModel by calling + get_init_model method. + :checkpoint checkpoint: dict containing the model & optimizer state dicts, + model_meta_data, etc.. + """ diff --git a/freqtrade/freqai/torch/PyTorchTransformerModel.py b/freqtrade/freqai/torch/PyTorchTransformerModel.py new file mode 100644 index 000000000..162459776 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchTransformerModel.py @@ -0,0 +1,93 @@ +import math + +import torch +from torch import nn + + +""" +The architecture is based on the paper “Attention Is All You Need”. +Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, +Lukasz Kaiser, and Illia Polosukhin. 2017. +""" + + +class PyTorchTransformerModel(nn.Module): + """ + A transformer approach to time series modeling using positional encoding. + The architecture is based on the paper “Attention Is All You Need”. + Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, + Lukasz Kaiser, and Illia Polosukhin. 2017. + """ + + def __init__(self, input_dim: int = 7, output_dim: int = 7, hidden_dim=1024, + n_layer=2, dropout_percent=0.1, time_window=10, nhead=8): + super().__init__() + self.time_window = time_window + # ensure the input dimension to the transformer is divisible by nhead + self.dim_val = input_dim - (input_dim % nhead) + self.input_net = nn.Sequential( + nn.Dropout(dropout_percent), nn.Linear(input_dim, self.dim_val) + ) + + # Encode the timeseries with Positional encoding + self.positional_encoding = PositionalEncoding(d_model=self.dim_val, max_len=self.dim_val) + + # Define the encoder block of the Transformer + self.encoder_layer = nn.TransformerEncoderLayer( + d_model=self.dim_val, nhead=nhead, dropout=dropout_percent, batch_first=True) + self.transformer = nn.TransformerEncoder(self.encoder_layer, num_layers=n_layer) + + # the pseudo decoding FC + self.output_net = nn.Sequential( + nn.Linear(self.dim_val * time_window, int(hidden_dim)), + nn.ReLU(), + nn.Dropout(dropout_percent), + nn.Linear(int(hidden_dim), int(hidden_dim / 2)), + nn.ReLU(), + nn.Dropout(dropout_percent), + nn.Linear(int(hidden_dim / 2), int(hidden_dim / 4)), + nn.ReLU(), + nn.Dropout(dropout_percent), + nn.Linear(int(hidden_dim / 4), output_dim) + ) + + def forward(self, x, mask=None, add_positional_encoding=True): + """ + Args: + x: Input features of shape [Batch, SeqLen, input_dim] + mask: Mask to apply on the attention outputs (optional) + add_positional_encoding: If True, we add the positional encoding to the input. + Might not be desired for some tasks. + """ + x = self.input_net(x) + if add_positional_encoding: + x = self.positional_encoding(x) + x = self.transformer(x, mask=mask) + x = x.reshape(-1, 1, self.time_window * x.shape[-1]) + x = self.output_net(x) + return x + + +class PositionalEncoding(nn.Module): + def __init__(self, d_model, max_len=5000): + """ + Args + d_model: Hidden dimensionality of the input. + max_len: Maximum length of a sequence to expect. + """ + super().__init__() + + # Create matrix of [SeqLen, HiddenDim] representing the positional encoding + # for max_len inputs + pe = torch.zeros(max_len, d_model) + position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) + div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + pe = pe.unsqueeze(0) + + self.register_buffer("pe", pe, persistent=False) + + def forward(self, x): + x = x + self.pe[:, : x.size(1)] + return x diff --git a/user_data/strategies/.gitkeep b/freqtrade/freqai/torch/__init__.py similarity index 100% rename from user_data/strategies/.gitkeep rename to freqtrade/freqai/torch/__init__.py diff --git a/freqtrade/freqai/torch/datasets.py b/freqtrade/freqai/torch/datasets.py new file mode 100644 index 000000000..120d8a116 --- /dev/null +++ b/freqtrade/freqai/torch/datasets.py @@ -0,0 +1,19 @@ +import torch + + +class WindowDataset(torch.utils.data.Dataset): + def __init__(self, xs, ys, window_size): + self.xs = xs + self.ys = ys + self.window_size = window_size + + def __len__(self): + return len(self.xs) - self.window_size + + def __getitem__(self, index): + idx_rev = len(self.xs) - self.window_size - index - 1 + window_x = self.xs[idx_rev:idx_rev + self.window_size, :] + # Beware of indexing, these two window_x and window_y are aimed at the same row! + # this is what happens when you use : + window_y = self.ys[idx_rev + self.window_size - 1, :].unsqueeze(0) + return window_x, window_y diff --git a/freqtrade/freqai/utils.py b/freqtrade/freqai/utils.py index 806e3ca15..b670a2aad 100644 --- a/freqtrade/freqai/utils.py +++ b/freqtrade/freqai/utils.py @@ -92,55 +92,6 @@ def get_required_data_timerange(config: Config) -> TimeRange: return data_load_timerange -# Keep below for when we wish to download heterogeneously lengthed data for FreqAI. -# def download_all_data_for_training(dp: DataProvider, config: Config) -> None: -# """ -# Called only once upon start of bot to download the necessary data for -# populating indicators and training a FreqAI model. -# :param timerange: TimeRange = The full data timerange for populating the indicators -# and training the model. -# :param dp: DataProvider instance attached to the strategy -# """ - -# if dp._exchange is not None: -# markets = [p for p, m in dp._exchange.markets.items() if market_is_active(m) -# or config.get('include_inactive')] -# else: -# # This should not occur: -# raise OperationalException('No exchange object found.') - -# all_pairs = dynamic_expand_pairlist(config, markets) - -# if not dp._exchange: -# # Not realistic - this is only called in live mode. -# raise OperationalException("Dataprovider did not have an exchange attached.") - -# time = datetime.now(tz=timezone.utc).timestamp() - -# for tf in config["freqai"]["feature_parameters"].get("include_timeframes"): -# timerange = TimeRange() -# timerange.startts = int(time) -# timerange.stopts = int(time) -# startup_candles = dp.get_required_startup(str(tf)) -# tf_seconds = timeframe_to_seconds(str(tf)) -# timerange.subtract_start(tf_seconds * startup_candles) -# new_pairs_days = int((timerange.stopts - timerange.startts) / 86400) -# # FIXME: now that we are looping on `refresh_backtest_ohlcv_data`, the function -# # redownloads the funding rate for each pair. -# refresh_backtest_ohlcv_data( -# dp._exchange, -# pairs=all_pairs, -# timeframes=[tf], -# datadir=config["datadir"], -# timerange=timerange, -# new_pairs_days=new_pairs_days, -# erase=False, -# data_format=config.get("dataformat_ohlcv", "json"), -# trading_mode=config.get("trading_mode", "spot"), -# prepend=config.get("prepend_data", False), -# ) - - def plot_feature_importance(model: Any, pair: str, dk: FreqaiDataKitchen, count_max: int = 25) -> None: """ @@ -211,7 +162,7 @@ def record_params(config: Dict[str, Any], full_path: Path) -> None: "pairs": config.get('exchange', {}).get('pair_whitelist') } - with open(params_record_path, "w") as handle: + with params_record_path.open("w") as handle: rapidjson.dump( run_params, handle, @@ -233,3 +184,13 @@ def get_timerange_backtest_live_models(config: Config) -> str: dd = FreqaiDataDrawer(models_path, config) timerange = dd.get_timerange_from_live_historic_predictions() return timerange.timerange_str + + +def get_tb_logger(model_type: str, path: Path, activate: bool) -> Any: + + if model_type == "pytorch" and activate: + from freqtrade.freqai.tensorboard import TBLogger + return TBLogger(path, activate) + else: + from freqtrade.freqai.tensorboard.base_tensorboard import BaseTensorboardLogger + return BaseTensorboardLogger(path, activate) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6e87db136..21426623f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1,9 +1,9 @@ """ Freqtrade is the main module of this bot. It contains the class Freqtrade() """ -import copy import logging import traceback +from copy import deepcopy from datetime import datetime, time, timedelta, timezone from math import isclose from threading import Lock @@ -13,7 +13,7 @@ from schedule import Scheduler from freqtrade import constants from freqtrade.configuration import validate_config_consistency -from freqtrade.constants import BuySell, Config, LongShort +from freqtrade.constants import BuySell, Config, ExchangeConfig, LongShort from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge @@ -21,18 +21,24 @@ from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, State, TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds +from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, timeframe_to_next_date, + timeframe_to_seconds) +from freqtrade.exchange.common import remove_exchange_credentials from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, init_db +from freqtrade.persistence.key_value_store import set_startup_time from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer +from freqtrade.rpc.rpc_types import (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg, + RPCSellMsg) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.util import FtPrecise +from freqtrade.util.binance_mig import migrate_binance_futures_names from freqtrade.wallets import Wallets @@ -58,6 +64,9 @@ class FreqtradeBot(LoggingMixin): # Init objects self.config = config + exchange_config: ExchangeConfig = deepcopy(config['exchange']) + # Remove credentials from original exchange config to avoid accidental credentail exposure + remove_exchange_credentials(config['exchange'], True) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) @@ -65,7 +74,7 @@ class FreqtradeBot(LoggingMixin): validate_config_consistency(config) self.exchange = ExchangeResolver.load_exchange( - self.config['exchange']['name'], self.config, load_leverage_tiers=True) + self.config, exchange_config=exchange_config, load_leverage_tiers=True) init_db(self.config['db_url']) @@ -126,19 +135,19 @@ class FreqtradeBot(LoggingMixin): for minutes in [0, 15, 30, 45]: t = str(time(time_slot, minutes, 2)) self._schedule.every().day.at(t).do(update) - self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc) + self.last_process: Optional[datetime] = None self.strategy.ft_bot_start() # Initialize protections AFTER bot start - otherwise parameters are not loaded. self.protections = ProtectionManager(self.config, self.strategy.protections) - def notify_status(self, msg: str) -> None: + def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None: """ Public method for users of this class (worker, etc.) to send notifications via RPC about changes in the bot status. """ self.rpc.send_msg({ - 'type': RPCMessageType.STATUS, + 'type': msg_type, 'status': msg }) @@ -177,6 +186,9 @@ class FreqtradeBot(LoggingMixin): Called on startup and after reloading the bot - triggers notifications and performs startup tasks """ + migrate_binance_futures_names(self.config) + set_startup_time() + self.rpc.startup_messages(self.config, self.pairlists, self.protections) # Update older trades with precision and precision mode self.startup_backpopulate_precision() @@ -209,7 +221,8 @@ class FreqtradeBot(LoggingMixin): self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), self.strategy.gather_informative_pairs()) - strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( + current_time=datetime.now(timezone.utc)) self.strategy.analyze(self.active_pair_whitelist) @@ -341,7 +354,15 @@ class FreqtradeBot(LoggingMixin): try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') - + if not order.trade: + # This should not happen, but it does if trades were deleted manually. + # This can only incur on sqlite, which doesn't enforce foreign constraints. + logger.warning( + f"Order {order.order_id} has no trade attached. " + "This may suggest a database corruption. " + f"The expected trade ID is {order.ft_trade_id}. Ignoring this order." + ) + continue self.update_trade_state(order.trade, order.order_id, fo, stoploss_order=(order.ft_order_side == 'stoploss')) @@ -352,7 +373,7 @@ class FreqtradeBot(LoggingMixin): "Order is older than 5 days. Assuming order was fully cancelled.") fo = order.to_ccxt_object() fo['status'] = 'canceled' - self.handle_timedout_order(fo, order.trade) + self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT']) except ExchangeError as e: @@ -403,7 +424,7 @@ class FreqtradeBot(LoggingMixin): """ Try refinding a lost trade. Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy). - Tries to walk the stored orders and sell them off eventually. + Tries to walk the stored orders and updates the trade state if necessary. """ logger.info(f"Trying to refind lost order for {trade}") for order in trade.orders: @@ -434,6 +455,42 @@ class FreqtradeBot(LoggingMixin): except ExchangeError: logger.warning(f"Error updating {order.order_id}.") + def handle_onexchange_order(self, trade: Trade): + """ + Try refinding a order that is not in the database. + Only used balance disappeared, which would make exiting impossible. + """ + try: + orders = self.exchange.fetch_orders(trade.pair, trade.open_date_utc) + for order in orders: + trade_order = [o for o in trade.orders if o.order_id == order['id']] + if trade_order: + continue + logger.info(f"Found previously unknown order {order['id']} for {trade.pair}.") + + order_obj = Order.parse_from_ccxt_object(order, trade.pair, order['side']) + order_obj.order_filled_date = datetime.fromtimestamp( + safe_value_fallback(order, 'lastTradeTimestamp', 'timestamp') // 1000, + tz=timezone.utc) + trade.orders.append(order_obj) + # TODO: how do we handle open_order_id ... + Trade.commit() + prev_exit_reason = trade.exit_reason + trade.exit_reason = ExitType.SOLD_ON_EXCHANGE.value + self.update_trade_state(trade, order['id'], order) + + logger.info(f"handled order {order['id']}") + if not trade.is_open: + # Trade was just closed + trade.close_date = order_obj.order_filled_date + Trade.commit() + break + else: + trade.exit_reason = prev_exit_reason + Trade.commit() + + except ExchangeError: + logger.warning("Error finding onexchange order") # # BUY / enter positions / open trades logic and methods # @@ -444,7 +501,7 @@ class FreqtradeBot(LoggingMixin): """ trades_created = 0 - whitelist = copy.deepcopy(self.active_pair_whitelist) + whitelist = deepcopy(self.active_pair_whitelist) if not whitelist: self.log_once("Active pair whitelist is empty.", logger.info) return trades_created @@ -473,7 +530,8 @@ class FreqtradeBot(LoggingMixin): # Create entity and execute trade for each pair from whitelist for pair in whitelist: try: - trades_created += self.create_trade(pair) + with self._exit_lock: + trades_created += self.create_trade(pair) except DependencyException as exception: logger.warning('Unable to create trade for %s: %s', pair, exception) @@ -575,7 +633,7 @@ class FreqtradeBot(LoggingMixin): min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_entry_rate, - self.strategy.stoploss) + 0.0) min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_exit_rate, self.strategy.stoploss) @@ -583,7 +641,7 @@ class FreqtradeBot(LoggingMixin): stake_available = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None)( + default_retval=None, supress_error=True)( trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_entry_rate, current_profit=current_entry_profit, min_stake=min_entry_stake, @@ -622,7 +680,7 @@ class FreqtradeBot(LoggingMixin): return remaining = (trade.amount - amount) * current_exit_rate - if remaining < min_exit_stake: + if min_exit_stake and remaining < min_exit_stake: logger.info(f"Remaining amount of {remaining} would be smaller " f"than the minimum of {min_exit_stake}.") return @@ -689,7 +747,8 @@ class FreqtradeBot(LoggingMixin): pos_adjust = trade is not None enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( - pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_) + pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_, + pos_adjust) if not stake_amount: return False @@ -747,13 +806,15 @@ class FreqtradeBot(LoggingMixin): self.exchange.name, order['filled'], order['amount'], order['remaining'] ) - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + amount = safe_value_fallback(order, 'filled', 'amount', amount) + enter_limit_filled_price = safe_value_fallback( + order, 'average', 'price', enter_limit_filled_price) # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + amount = safe_value_fallback(order, 'filled', 'amount', amount) + enter_limit_filled_price = safe_value_fallback( + order, 'average', 'price', enter_limit_requested) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') @@ -796,6 +857,9 @@ class FreqtradeBot(LoggingMixin): precision_mode=self.exchange.precisionMode, contract_size=self.exchange.get_contract_size(pair), ) + stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair) + trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True) + else: # This is additional buy, we reset fee_open_currency so timeout checking can work trade.is_open = True @@ -805,7 +869,7 @@ class FreqtradeBot(LoggingMixin): trade.orders.append(order_obj) trade.recalc_trade_from_orders() - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() # Updating wallets @@ -828,16 +892,18 @@ class FreqtradeBot(LoggingMixin): def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade: # First cancelling stoploss on exchange ... - if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: + if trade.stoploss_order_id: try: logger.info(f"Canceling stoploss on exchange for {trade}") co = self.exchange.cancel_stoploss_order_with_result( trade.stoploss_order_id, trade.pair, trade.amount) - trade.update_order(co) + self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True) + # Reset stoploss order id. trade.stoploss_order_id = None except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") + logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} " + f"for pair {trade.pair}") return trade def get_valid_enter_price_and_stake( @@ -847,7 +913,12 @@ class FreqtradeBot(LoggingMixin): trade: Optional[Trade], order_adjust: bool, leverage_: Optional[float], + pos_adjust: bool, ) -> Tuple[float, float, float]: + """ + Validate and eventually adjust (within limits) limit, amount and leverage + :return: Tuple with (price, amount, leverage) + """ if price: enter_limit_requested = price @@ -893,7 +964,9 @@ class FreqtradeBot(LoggingMixin): # We do however also need min-stake to determine leverage, therefore this is ignored as # edge-case for now. min_stake_amount = self.exchange.get_min_pair_stake_amount( - pair, enter_limit_requested, self.strategy.stoploss, leverage) + pair, enter_limit_requested, + self.strategy.stoploss if not pos_adjust else 0.0, + leverage) max_stake_amount = self.exchange.get_max_pair_stake_amount( pair, enter_limit_requested, leverage) @@ -917,12 +990,11 @@ class FreqtradeBot(LoggingMixin): return enter_limit_requested, stake_amount, leverage - def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None, + def _notify_enter(self, trade: Trade, order: Order, order_type: str, fill: bool = False, sub_trade: bool = False) -> None: """ Sends rpc notification when a entry order occurred. """ - msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY open_rate = order.safe_price if open_rate is None: @@ -933,9 +1005,9 @@ class FreqtradeBot(LoggingMixin): current_rate = self.exchange.get_rate( trade.pair, side='entry', is_short=trade.is_short, refresh=False) - msg = { + msg: RPCBuyMsg = { 'trade_id': trade.id, - 'type': msg_type, + 'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY, 'buy_tag': trade.enter_tag, 'enter_tag': trade.enter_tag, 'exchange': trade.exchange.capitalize(), @@ -947,9 +1019,10 @@ class FreqtradeBot(LoggingMixin): 'order_type': order_type, 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount), - 'open_date': trade.open_date or datetime.utcnow(), + 'open_date': trade.open_date_utc or datetime.now(timezone.utc), 'current_rate': current_rate, 'sub_trade': sub_trade, } @@ -965,7 +1038,7 @@ class FreqtradeBot(LoggingMixin): current_rate = self.exchange.get_rate( trade.pair, side='entry', is_short=trade.is_short, refresh=False) - msg = { + msg: RPCCancelMsg = { 'trade_id': trade.id, 'type': RPCMessageType.ENTRY_CANCEL, 'buy_tag': trade.enter_tag, @@ -977,7 +1050,9 @@ class FreqtradeBot(LoggingMixin): 'limit': trade.open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, + 'open_rate': trade.open_rate, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': trade.amount, 'open_date': trade.open_date, @@ -999,13 +1074,24 @@ class FreqtradeBot(LoggingMixin): """ trades_closed = 0 for trade in trades: - try: - if (self.strategy.order_types.get('stoploss_on_exchange') and - self.handle_stoploss_on_exchange(trade)): - trades_closed += 1 - Trade.commit() - continue + if not self.wallets.check_exit_amount(trade): + logger.warning( + f'Not enough {trade.safe_base_currency} in wallet to exit {trade}. ' + 'Trying to recover.') + self.handle_onexchange_order(trade) + + try: + try: + if (self.strategy.order_types.get('stoploss_on_exchange') and + self.handle_stoploss_on_exchange(trade)): + trades_closed += 1 + Trade.commit() + continue + + except InvalidOrderException as exception: + logger.warning( + f'Unable to handle stoploss on exchange for {trade.pair}: {exception}') # Check if we can sell our current pair if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): trades_closed += 1 @@ -1065,7 +1151,7 @@ class FreqtradeBot(LoggingMixin): datetime.now(timezone.utc), enter=enter, exit_=exit_, - force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 + force_stoploss=self.edge.get_stoploss(trade.pair) if self.edge else 0 ) for should_exit in exits: if should_exit.exit_flag: @@ -1085,7 +1171,7 @@ class FreqtradeBot(LoggingMixin): :return: True if the order succeeded, and False in case of problems. """ try: - stoploss_order = self.exchange.stoploss( + stoploss_order = self.exchange.create_stoploss( pair=trade.pair, amount=trade.amount, stop_price=stop_price, @@ -1109,8 +1195,7 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple( - exit_type=ExitType.EMERGENCY_EXIT)) + self.emergency_exit(trade, stop_price) except ExchangeError: trade.stoploss_order_id = None @@ -1138,7 +1223,8 @@ class FreqtradeBot(LoggingMixin): logger.warning('Unable to fetch stoploss order: %s', exception) if stoploss_order: - trade.update_order(stoploss_order) + self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, + stoploss_order=True) # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): @@ -1157,15 +1243,13 @@ class FreqtradeBot(LoggingMixin): # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange if not stoploss_order: - stoploss = ( - self.edge.stoploss(pair=trade.pair) - if self.edge else - trade.stop_loss_pct / trade.leverage - ) - if trade.is_short: - stop_price = trade.open_rate * (1 - stoploss) - else: - stop_price = trade.open_rate * (1 + stoploss) + stop_price = trade.stoploss_or_liquidation + if self.edge: + stoploss = self.edge.get_stoploss(pair=trade.pair) + stop_price = ( + trade.open_rate * (1 - stoploss) if trade.is_short + else trade.open_rate * (1 + stoploss) + ) if self.create_stoploss_order(trade=trade, stop_price=stop_price): # The above will return False if the placement failed and the trade was force-sold. @@ -1204,7 +1288,9 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation) + stoploss_norm = self.exchange.price_to_precision( + trade.pair, trade.stoploss_or_liquidation, + rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP) if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side): # we check if the update is necessary @@ -1214,13 +1300,8 @@ class FreqtradeBot(LoggingMixin): # cancelling the current stoploss on exchange first logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " f"(orderid:{order['id']}) in order to add another one ...") - try: - co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair, - trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {order['id']} " - f"for pair {trade.pair}") + + self.cancel_stoploss_on_exchange(trade) # Create new stoploss order if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm): @@ -1250,11 +1331,11 @@ class FreqtradeBot(LoggingMixin): if not_closed: if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( trade, order_obj, datetime.now(timezone.utc))): - self.handle_timedout_order(order, trade) + self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT']) else: self.replace_order(order, order_obj, trade) - def handle_timedout_order(self, order: Dict, trade: Trade) -> None: + def handle_cancel_order(self, order: Dict, trade: Trade, reason: str) -> None: """ Check if current analyzed order timed out and cancel if necessary. :param order: Order dict grabbed with exchange.fetch_order() @@ -1262,22 +1343,24 @@ class FreqtradeBot(LoggingMixin): :return: None """ if order['side'] == trade.entry_side: - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) + self.handle_cancel_enter(trade, order, reason) else: - canceled = self.handle_cancel_exit( - trade, order, constants.CANCEL_REASON['TIMEOUT']) + canceled = self.handle_cancel_exit(trade, order, reason) canceled_count = trade.get_exit_order_count() max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: logger.warning(f'Emergency exiting trade {trade}, as the exit order ' f'timed out {max_timeouts} times.') - try: - self.execute_trade_exit( - trade, order['price'], - exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) - except DependencyException as exception: - logger.warning( - f'Unable to emergency sell trade {trade.pair}: {exception}') + self.emergency_exit(trade, order['price']) + + def emergency_exit(self, trade: Trade, price: float) -> None: + try: + self.execute_trade_exit( + trade, price, + exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) + except DependencyException as exception: + logger.warning( + f'Unable to emergency exit trade {trade.pair}: {exception}') def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None: """ @@ -1304,7 +1387,7 @@ class FreqtradeBot(LoggingMixin): default_retval=order_obj.price)( trade=trade, order=order_obj, pair=trade.pair, current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate, - current_order_rate=order_obj.price, entry_tag=trade.enter_tag, + current_order_rate=order_obj.safe_price, entry_tag=trade.enter_tag, side=trade.entry_side) replacing = True @@ -1320,7 +1403,8 @@ class FreqtradeBot(LoggingMixin): # place new order only if new price is supplied self.execute_entry( pair=trade.pair, - stake_amount=(order_obj.remaining * order_obj.price / trade.leverage), + stake_amount=( + order_obj.safe_remaining * order_obj.safe_price / trade.leverage), price=adjusted_entry_price, trade=trade, is_short=trade.is_short, @@ -1334,6 +1418,8 @@ class FreqtradeBot(LoggingMixin): """ for trade in Trade.get_open_order_trades(): + if not trade.open_order_id: + continue try: order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (ExchangeError): @@ -1358,6 +1444,9 @@ class FreqtradeBot(LoggingMixin): """ was_trade_fully_canceled = False side = trade.entry_side.capitalize() + if not trade.open_order_id: + logger.warning(f"No open order for {trade}.") + return False # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: @@ -1384,7 +1473,7 @@ class FreqtradeBot(LoggingMixin): corder = order reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('%s order %s for %s.', side, reason, trade) + logger.info(f'{side} order {reason} for {trade}.') # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') @@ -1444,35 +1533,34 @@ class FreqtradeBot(LoggingMixin): return False try: - co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, - trade.amount) + order = self.exchange.cancel_order_with_result( + order['id'], trade.pair, trade.amount) except InvalidOrderException: logger.exception( f"Could not cancel {trade.exit_side} order {trade.open_order_id}") return False - trade.close_rate = None - trade.close_rate_requested = None - trade.close_profit = None - trade.close_profit_abs = None + # Set exit_reason for fill message exit_reason_prev = trade.exit_reason trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason - self.update_trade_state(trade, trade.open_order_id, co) # Order might be filled above in odd timing issues. - if co.get('status') in ('canceled', 'cancelled'): + if order.get('status') in ('canceled', 'cancelled'): trade.exit_reason = None trade.open_order_id = None else: trade.exit_reason = exit_reason_prev - - logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') cancelled = True else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') - self.update_trade_state(trade, trade.open_order_id, order) + trade.exit_reason = None trade.open_order_id = None + self.update_trade_state(trade, order['id'], order) + + logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') + trade.close_rate = None + trade.close_rate_requested = None + self._notify_exit_cancel( trade, order_type=self.strategy.order_types['exit'], @@ -1495,13 +1583,13 @@ class FreqtradeBot(LoggingMixin): # Update wallets to ensure amounts tied up in a stoploss is now free! self.wallets.update() if self.trading_mode == TradingMode.FUTURES: + # A safe exit amount isn't needed for futures, you can just exit/close the position return amount trade_base_currency = self.exchange.get_pair_base_currency(pair) wallet_amount = self.wallets.get_free(trade_base_currency) logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") if wallet_amount >= amount: - # A safe exit amount isn't needed for futures, you can just exit/close the position return amount elif wallet_amount > amount * 0.98: logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.") @@ -1519,7 +1607,7 @@ class FreqtradeBot(LoggingMixin): *, exit_tag: Optional[str] = None, ordertype: Optional[str] = None, - sub_trade_amt: float = None, + sub_trade_amt: Optional[float] = None, ) -> bool: """ Executes a trade exit for the given trade and limit @@ -1613,7 +1701,7 @@ class FreqtradeBot(LoggingMixin): return True def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False, - sub_trade: bool = False, order: Order = None) -> None: + sub_trade: bool = False, order: Optional[Order] = None) -> None: """ Sends rpc notification when a sell occurred. """ @@ -1623,19 +1711,19 @@ class FreqtradeBot(LoggingMixin): # second condition is for mypy only; order will always be passed during sub trade if sub_trade and order is not None: - amount = order.safe_filled if fill else order.amount + amount = order.safe_filled if fill else order.safe_amount order_rate: float = order.safe_price profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate) profit_ratio = trade.calc_profit_ratio(order_rate, amount, trade.open_rate) else: - order_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + order_rate = trade.safe_close_rate profit = trade.calc_profit(rate=order_rate) + (0.0 if fill else trade.realized_profit) profit_ratio = trade.calc_profit_ratio(order_rate) amount = trade.amount gain = "profit" if profit_ratio > 0 else "loss" - msg = { + msg: RPCSellMsg = { 'type': (RPCMessageType.EXIT_FILL if fill else RPCMessageType.EXIT), 'trade_id': trade.id, @@ -1657,10 +1745,11 @@ class FreqtradeBot(LoggingMixin): 'enter_tag': trade.enter_tag, 'sell_reason': trade.exit_reason, # Deprecated 'exit_reason': trade.exit_reason, - 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.utcnow(), + 'open_date': trade.open_date_utc, + 'close_date': trade.close_date_utc or datetime.now(timezone.utc), 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency'), 'sub_trade': sub_trade, 'cumulative_profit': trade.realized_profit, @@ -1679,19 +1768,17 @@ class FreqtradeBot(LoggingMixin): else: trade.exit_order_status = reason - order = trade.select_order_by_order_id(order_id) - if not order: - raise DependencyException( - f"Order_obj not found for {order_id}. This should not have happened.") + order_or_none = trade.select_order_by_order_id(order_id) + order = self.order_obj_or_raise(order_id, order_or_none) - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit_rate: float = trade.safe_close_rate profit_trade = trade.calc_profit(rate=profit_rate) current_rate = self.exchange.get_rate( trade.pair, side='exit', is_short=trade.is_short, refresh=False) profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" - msg = { + msg: RPCSellCancelMsg = { 'type': RPCMessageType.EXIT_CANCEL, 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), @@ -1713,6 +1800,7 @@ class FreqtradeBot(LoggingMixin): 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], + 'base_currency': self.exchange.get_pair_base_currency(trade.pair), 'fiat_currency': self.config.get('fiat_display_currency', None), 'reason': reason, 'sub_trade': sub_trade, @@ -1722,12 +1810,20 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) + def order_obj_or_raise(self, order_id: str, order_obj: Optional[Order]) -> Order: + if not order_obj: + raise DependencyException( + f"Order_obj not found for {order_id}. This should not have happened.") + return order_obj + # # Common update trade state methods # - def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, - stoploss_order: bool = False, send_msg: bool = True) -> bool: + def update_trade_state( + self, trade: Trade, order_id: Optional[str], + action_order: Optional[Dict[str, Any]] = None, + stoploss_order: bool = False, send_msg: bool = True) -> bool: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. @@ -1742,11 +1838,11 @@ class FreqtradeBot(LoggingMixin): return False # Update trade with order values - logger.info(f'Found open order for {trade}') + if not stoploss_order: + logger.info(f'Found open order for {trade}') try: - order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, - trade.pair, - stoploss_order) + order = action_order or self.exchange.fetch_order_or_stoploss_order( + order_id, trade.pair, stoploss_order) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False @@ -1758,10 +1854,8 @@ class FreqtradeBot(LoggingMixin): # Handling of this will happen in check_handle_timedout. return True - order_obj = trade.select_order_by_order_id(order_id) - if not order_obj: - raise DependencyException( - f"Order_obj not found for {order_id}. This should not have happened.") + order_obj_or_none = trade.select_order_by_order_id(order_id) + order_obj = self.order_obj_or_raise(order_id, order_obj_or_none) self.handle_order_fee(trade, order_obj, order) @@ -1775,19 +1869,22 @@ class FreqtradeBot(LoggingMixin): # TODO: should shorting/leverage be supported by Edge, # then this will need to be fixed. trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - if order.get('side') == trade.entry_side or trade.amount > 0: + if order.get('side') == trade.entry_side or (trade.amount > 0 and trade.is_open): # Must also run for partial exits # TODO: Margin will need to use interest_rate as well. # interest_rate = self.exchange.get_interest_rate() - trade.set_liquidation_price(self.exchange.get_liquidation_price( - pair=trade.pair, - open_rate=trade.open_rate, - is_short=trade.is_short, - amount=trade.amount, - stake_amount=trade.stake_amount, - wallet_balance=trade.stake_amount, - )) - + try: + trade.set_liquidation_price(self.exchange.get_liquidation_price( + pair=trade.pair, + open_rate=trade.open_rate, + is_short=trade.is_short, + amount=trade.amount, + stake_amount=trade.stake_amount, + leverage=trade.leverage, + wallet_balance=trade.stake_amount, + )) + except DependencyException: + logger.warning('Unable to calculate liquidation price') # Updating wallets when order is closed self.wallets.update() Trade.commit() @@ -1810,21 +1907,27 @@ class FreqtradeBot(LoggingMixin): self.handle_protections(trade.pair, trade.trade_direction) elif send_msg and not trade.open_order_id and not stoploss_order: # Enter fill - self._notify_enter(trade, order, fill=True, sub_trade=sub_trade) + self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade) def handle_protections(self, pair: str, side: LongShort) -> None: # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock') prot_trig = self.protections.stop_per_pair(pair, side=side) if prot_trig: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER, } - msg.update(prot_trig.to_json()) + msg: RPCProtectionMsg = { + 'type': RPCMessageType.PROTECTION_TRIGGER, + 'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair), + **prot_trig.to_json() # type: ignore + } self.rpc.send_msg(msg) prot_trig_glb = self.protections.global_stop(side=side) if prot_trig_glb: - msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, } - msg.update(prot_trig_glb.to_json()) + msg = { + 'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, + 'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair), + **prot_trig_glb.to_json() # type: ignore + } self.rpc.send_msg(msg) def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index ae78f4722..d4526dbec 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1,2 +1 @@ -# flake8: noqa: F401 -from freqtrade.leverage.interest import interest +from freqtrade.leverage.interest import interest # noqa: F401 diff --git a/freqtrade/loggers.py b/freqtrade/loggers/__init__.py similarity index 85% rename from freqtrade/loggers.py rename to freqtrade/loggers/__init__.py index f365053c9..58f207608 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers/__init__.py @@ -1,24 +1,11 @@ import logging -import sys from logging import Formatter -from logging.handlers import BufferingHandler, RotatingFileHandler, SysLogHandler +from logging.handlers import RotatingFileHandler, SysLogHandler from freqtrade.constants import Config from freqtrade.exceptions import OperationalException - - -class FTBufferingHandler(BufferingHandler): - def flush(self): - """ - Override Flush behaviour - we keep half of the configured capacity - otherwise, we have moments with "empty" logs. - """ - self.acquire() - try: - # Keep half of the records in buffer. - self.buffer = self.buffer[-int(self.capacity / 2):] - finally: - self.release() +from freqtrade.loggers.buffering_handler import FTBufferingHandler +from freqtrade.loggers.std_err_stream_handler import FTStdErrStreamHandler logger = logging.getLogger(__name__) @@ -45,6 +32,7 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None: logging.INFO if verbosity <= 2 else logging.DEBUG ) logging.getLogger('telegram').setLevel(logging.INFO) + logging.getLogger('httpx').setLevel(logging.INFO) logging.getLogger('werkzeug').setLevel( logging.ERROR if api_verbosity == 'error' else logging.INFO @@ -69,7 +57,7 @@ def setup_logging_pre() -> None: logging.basicConfig( level=logging.INFO, format=LOGFORMAT, - handlers=[logging.StreamHandler(sys.stderr), bufferHandler] + handlers=[FTStdErrStreamHandler(), bufferHandler] ) @@ -103,9 +91,9 @@ def setup_logging(config: Config) -> None: logging.root.addHandler(handler_sl) elif s[0] == 'journald': # pragma: no cover try: - from systemd.journal import JournaldLogHandler + from cysystemd.journal import JournaldLogHandler except ImportError: - raise OperationalException("You need the systemd python package be installed in " + raise OperationalException("You need the cysystemd python package be installed in " "order to use logging to journald.") handler_jd = get_existing_handlers(JournaldLogHandler) if handler_jd: diff --git a/freqtrade/loggers/buffering_handler.py b/freqtrade/loggers/buffering_handler.py new file mode 100644 index 000000000..e4621fa79 --- /dev/null +++ b/freqtrade/loggers/buffering_handler.py @@ -0,0 +1,15 @@ +from logging.handlers import BufferingHandler + + +class FTBufferingHandler(BufferingHandler): + def flush(self): + """ + Override Flush behaviour - we keep half of the configured capacity + otherwise, we have moments with "empty" logs. + """ + self.acquire() + try: + # Keep half of the records in buffer. + self.buffer = self.buffer[-int(self.capacity / 2):] + finally: + self.release() diff --git a/freqtrade/loggers/std_err_stream_handler.py b/freqtrade/loggers/std_err_stream_handler.py new file mode 100644 index 000000000..487a7c100 --- /dev/null +++ b/freqtrade/loggers/std_err_stream_handler.py @@ -0,0 +1,26 @@ +import sys +from logging import Handler + + +class FTStdErrStreamHandler(Handler): + def flush(self): + """ + Override Flush behaviour - we keep half of the configured capacity + otherwise, we have moments with "empty" logs. + """ + self.acquire() + try: + sys.stderr.flush() + finally: + self.release() + + def emit(self, record): + try: + msg = self.format(record) + # Don't keep a reference to stderr - this can be problematic with progressbars. + sys.stderr.write(msg + '\n') + self.flush() + except RecursionError: + raise + except Exception: + self.handleError(record) diff --git a/freqtrade/main.py b/freqtrade/main.py index 0a46747ea..a10620498 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -5,7 +5,7 @@ Read the documentation to know what cli arguments you need. """ import logging import sys -from typing import Any, List +from typing import Any, List, Optional from freqtrade.util.gc_setup import gc_set_threshold @@ -23,7 +23,7 @@ from freqtrade.loggers import setup_logging_pre logger = logging.getLogger('freqtrade') -def main(sysargv: List[str] = None) -> None: +def main(sysargv: Optional[List[str]] = None) -> None: """ This function will initiate the bot and start the trading loop. :return: None diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 7b9ff1f1d..1e84bba87 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -3,11 +3,9 @@ Various tool function for Freqtrade and scripts """ import gzip import logging -import re from datetime import datetime from pathlib import Path -from typing import Any, Dict, Iterator, List, Mapping, Union -from typing.io import IO +from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union from urllib.parse import urlparse import pandas as pd @@ -48,18 +46,6 @@ def round_coin_value( return val -def shorten_date(_date: str) -> str: - """ - Trim the date so it fits on small screens - """ - new_date = re.sub('seconds?', 'sec', _date) - new_date = re.sub('minutes?', 'min', new_date) - new_date = re.sub('hours?', 'h', new_date) - new_date = re.sub('days?', 'd', new_date) - new_date = re.sub('^an?', '1', new_date) - return new_date - - def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None: """ Dump JSON data into a file @@ -80,7 +66,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = else: if log: logger.info(f'dumping json to "{filename}"') - with open(filename, 'w') as fp: + with filename.open('w') as fp: rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) logger.debug(f'done json to "{filename}"') @@ -97,12 +83,12 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None: if log: logger.info(f'dumping joblib to "{filename}"') - with open(filename, 'wb') as fp: + with filename.open('wb') as fp: joblib.dump(data, fp) logger.debug(f'done joblib dump to "{filename}"') -def json_load(datafile: IO) -> Any: +def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any: """ load data with rapidjson Use this to have a consistent experience, @@ -111,7 +97,7 @@ def json_load(datafile: IO) -> Any: return rapidjson.load(datafile, number_mode=rapidjson.NM_NATIVE) -def file_load_json(file): +def file_load_json(file: Path): if file.suffix != ".gz": gzipfile = file.with_suffix(file.suffix + '.gz') @@ -124,7 +110,7 @@ def file_load_json(file): pairdata = json_load(datafile) elif file.is_file(): logger.debug(f"Loading historical data from file {file}") - with open(file) as datafile: + with file.open() as datafile: pairdata = json_load(datafile) else: return None @@ -204,7 +190,7 @@ def safe_value_fallback2(dict1: dictMap, dict2: dictMap, key1: str, key2: str, d return default_value -def plural(num: float, singular: str, plural: str = None) -> str: +def plural(num: float, singular: str, plural: Optional[str] = None) -> str: return singular if (num == 1 or num == -1) else plural or singular + 's' diff --git a/freqtrade/mixins/__init__.py b/freqtrade/mixins/__init__.py index f4a640fa3..c5363c076 100644 --- a/freqtrade/mixins/__init__.py +++ b/freqtrade/mixins/__init__.py @@ -1,2 +1 @@ -# flake8: noqa: F401 -from freqtrade.mixins.logging_mixin import LoggingMixin +from freqtrade.mixins.logging_mixin import LoggingMixin # noqa: F401 diff --git a/freqtrade/optimize/backtest_caching.py b/freqtrade/optimize/backtest_caching.py index d9d270072..f34bbffef 100644 --- a/freqtrade/optimize/backtest_caching.py +++ b/freqtrade/optimize/backtest_caching.py @@ -29,7 +29,7 @@ def get_strategy_run_id(strategy) -> str: # Include _ft_params_from_file - so changing parameter files cause cache eviction digest.update(rapidjson.dumps( strategy._ft_params_from_file, default=str, number_mode=rapidjson.NM_NAN).encode('utf-8')) - with open(strategy.__file__, 'rb') as fp: + with Path(strategy.__file__).open('rb') as fp: digest.update(fp.read()) return digest.hexdigest().lower() diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 394960042..d77fc469b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -9,13 +9,12 @@ from copy import deepcopy from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple -import pandas as pd from numpy import nan from pandas import DataFrame from freqtrade import constants from freqtrade.configuration import TimeRange, validate_config_consistency -from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, LongShort +from freqtrade.constants import DATETIME_PRINT_FORMAT, Config, IntOrInf, LongShort from freqtrade.data import history from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes @@ -28,8 +27,10 @@ from freqtrade.exchange import (amount_to_contract_precision, price_to_precision from freqtrade.mixins import LoggingMixin from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.bt_progress import BTProgress -from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, - store_backtest_signal_candles, +from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_rejected_signals, + generate_trade_signal_candles, + show_backtest_results, + store_backtest_analysis_results, store_backtest_stats) from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager @@ -37,6 +38,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.util.binance_mig import migrate_binance_futures_data from freqtrade.wallets import Wallets @@ -83,16 +85,17 @@ class Backtesting: self.strategylist: List[IStrategy] = [] self.all_results: Dict[str, Dict] = {} self.processed_dfs: Dict[str, Dict] = {} + self.rejected_dict: Dict[str, List] = {} + self.rejected_df: Dict[str, Dict] = {} self._exchange_name = self.config['exchange']['name'] - self.exchange = ExchangeResolver.load_exchange( - self._exchange_name, self.config, load_leverage_tiers=True) + self.exchange = ExchangeResolver.load_exchange(self.config, load_leverage_tiers=True) self.dataprovider = DataProvider(self.config, self.exchange) if self.config.get('strategy_list'): if self.config.get('freqai', {}).get('enabled', False): logger.warning("Using --strategy-list with FreqAI REQUIRES all strategies " - "to have identical populate_any_indicators.") + "to have identical feature_engineering_* functions.") for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat @@ -157,6 +160,7 @@ class Backtesting: self._can_short = self.trading_mode != TradingMode.SPOT self._position_stacking: bool = self.config.get('position_stacking', False) self.enable_protections: bool = self.config.get('enable_protections', False) + migrate_binance_futures_data(config) self.init_backtest() @@ -201,9 +205,10 @@ class Backtesting: # since a "perfect" stoploss-exit is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + # Update can_short flag + self._can_short = self.trading_mode != TradingMode.SPOT and strategy.can_short self.strategy.ft_bot_start() - strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() def _load_protections(self, strategy: IStrategy): if self.config.get('enable_protections', False): @@ -438,11 +443,8 @@ class Backtesting: side_1 * abs(self.strategy.trailing_stop_positive / leverage))) else: # Worst case: price ticks tiny bit above open and dives down. - stop_rate = row[OPEN_IDX] * (1 - side_1 * abs(trade.stop_loss_pct / leverage)) - if is_short: - assert stop_rate > row[LOW_IDX] - else: - assert stop_rate < row[HIGH_IDX] + stop_rate = row[OPEN_IDX] * (1 - side_1 * abs( + (trade.stop_loss_pct or 0.0) / leverage)) # Limit lower-end to candle low to avoid exits below the low. # This still remains "worst case" - but "worst realistic case". @@ -470,7 +472,7 @@ class Backtesting: # - (Expected abs profit - open_rate - open_fee) / (fee_close -1) roi_rate = trade.open_rate * roi / leverage open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open) - close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1) + close_rate = -(roi_rate + open_fee_rate) / ((trade.fee_close or 0.0) - side_1 * 1) if is_short: is_new_roi = row[OPEN_IDX] < close_rate else: @@ -523,7 +525,7 @@ class Backtesting: max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) stake_available = self.wallets.get_available_stake_amount() stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None)( + default_retval=None, supress_error=True)( trade=trade, # type: ignore[arg-type] current_time=current_date, current_rate=current_rate, current_profit=current_profit, min_stake=min_stake, @@ -561,7 +563,7 @@ class Backtesting: pos_trade = self._get_exit_for_signal(trade, row, exit_, amount) if pos_trade is not None: order = pos_trade.orders[-1] - if self._get_order_filled(order.price, row): + if self._get_order_filled(order.ft_price, row): order.close_bt_order(current_date, trade) trade.recalc_trade_from_orders() self.wallets.update() @@ -573,26 +575,6 @@ class Backtesting: """ Rate is within candle, therefore filled""" return row[LOW_IDX] <= rate <= row[HIGH_IDX] - def _get_exit_trade_entry_for_candle(self, trade: LocalTrade, - row: Tuple) -> Optional[LocalTrade]: - - # Check if we need to adjust our current positions - if self.strategy.position_adjustment_enable: - trade = self._get_adjust_trade_entry_for_candle(trade, row) - - enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX] - exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX] - exits = self.strategy.should_exit( - trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore - enter=enter, exit_=exit_sig, - low=row[LOW_IDX], high=row[HIGH_IDX] - ) - for exit_ in exits: - t = self._get_exit_for_signal(trade, row, exit_) - if t: - return t - return None - def _get_exit_for_signal( self, trade: LocalTrade, row: Tuple, exit_: ExitCheckTuple, amount: Optional[float] = None) -> Optional[LocalTrade]: @@ -662,7 +644,7 @@ class Backtesting: return None def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, - close_rate: float, amount: float = None) -> Optional[LocalTrade]: + close_rate: float, amount: Optional[float] = None) -> Optional[LocalTrade]: self.order_id_counter += 1 exit_candle_time = sell_row[DATE_IDX].to_pydatetime() order_type = self.strategy.order_types['exit'] @@ -682,6 +664,7 @@ class Backtesting: side=trade.exit_side, order_type=order_type, status="open", + ft_price=close_rate, price=close_rate, average=close_rate, amount=amount, @@ -692,11 +675,10 @@ class Backtesting: trade.orders.append(order) return trade - def _get_exit_trade_entry( - self, trade: LocalTrade, row: Tuple, is_first: bool) -> Optional[LocalTrade]: + def _check_trade_exit(self, trade: LocalTrade, row: Tuple) -> Optional[LocalTrade]: exit_candle_time: datetime = row[DATE_IDX].to_pydatetime() - if is_first and self.trading_mode == TradingMode.FUTURES: + if self.trading_mode == TradingMode.FUTURES: trade.funding_fees = self.exchange.calculate_funding_fees( self.futures_data[trade.pair], amount=trade.amount, @@ -705,7 +687,22 @@ class Backtesting: close_date=exit_candle_time, ) - return self._get_exit_trade_entry_for_candle(trade, row) + # Check if we need to adjust our current positions + if self.strategy.position_adjustment_enable: + trade = self._get_adjust_trade_entry_for_candle(trade, row) + + enter = row[SHORT_IDX] if trade.is_short else row[LONG_IDX] + exit_sig = row[ESHORT_IDX] if trade.is_short else row[ELONG_IDX] + exits = self.strategy.should_exit( + trade, row[OPEN_IDX], row[DATE_IDX].to_pydatetime(), # type: ignore + enter=enter, exit_=exit_sig, + low=row[LOW_IDX], high=row[HIGH_IDX] + ) + for exit_ in exits: + t = self._get_exit_for_signal(trade, row, exit_) + if t: + return t + return None def get_valid_price_and_stake( self, pair: str, row: Tuple, propose_rate: float, stake_amount: float, @@ -746,12 +743,12 @@ class Backtesting: proposed_leverage=1.0, max_leverage=max_leverage, side=direction, entry_tag=entry_tag, - ) if self._can_short else 1.0 + ) if self.trading_mode != TradingMode.SPOT else 1.0 # Cap leverage between 1.0 and max_leverage. leverage = min(max(leverage, 1.0), max_leverage) min_stake_amount = self.exchange.get_min_pair_stake_amount( - pair, propose_rate, -0.05, leverage=leverage) or 0 + pair, propose_rate, -0.05 if not pos_adjust else 0.0, leverage=leverage) or 0 max_stake_amount = self.exchange.get_max_pair_stake_amount( pair, propose_rate, leverage=leverage) stake_available = self.wallets.get_available_stake_amount() @@ -779,6 +776,11 @@ class Backtesting: trade: Optional[LocalTrade] = None, requested_rate: Optional[float] = None, requested_stake: Optional[float] = None) -> Optional[LocalTrade]: + """ + :param trade: Trade to adjust - initial entry if None + :param requested_rate: Adjusted entry rate + :param requested_stake: Stake amount for adjusted orders (`adjust_entry_price`). + """ current_time = row[DATE_IDX].to_pydatetime() entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None @@ -804,7 +806,7 @@ class Backtesting: return trade time_in_force = self.strategy.order_time_in_force['entry'] - if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): + if stake_amount and (not min_stake_amount or stake_amount >= min_stake_amount): self.order_id_counter += 1 base_currency = self.exchange.get_pair_base_currency(pair) amount_p = (stake_amount / propose_rate) * leverage @@ -867,6 +869,7 @@ class Backtesting: open_rate=propose_rate, amount=amount, stake_amount=trade.stake_amount, + leverage=trade.leverage, wallet_balance=trade.stake_amount, is_short=is_short, )) @@ -885,6 +888,7 @@ class Backtesting: order_date=current_time, order_filled_date=current_time, order_update_date=current_time, + ft_price=propose_rate, price=propose_rate, average=propose_rate, amount=amount, @@ -893,7 +897,7 @@ class Backtesting: cost=stake_amount + trade.fee_open, ) trade.orders.append(order) - if pos_adjust and self._get_order_filled(order.price, row): + if pos_adjust and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_time, trade) else: trade.open_order_id = str(self.order_id_counter) @@ -920,8 +924,9 @@ class Backtesting: trade.close(exit_row[OPEN_IDX], show_msg=False) LocalTrade.close_bt_trade(trade) - def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: + def trade_slot_available(self, open_trade_count: int) -> bool: # Always allow trades when max_open_trades is enabled. + max_open_trades: IntOrInf = self.config['max_open_trades'] if max_open_trades <= 0 or open_trade_count < max_open_trades: return True # Rejected trade @@ -1005,15 +1010,15 @@ class Backtesting: # only check on new candles for open entry orders if order.side == trade.entry_side and current_time > order.order_date_utc: requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price, - default_retval=order.price)( + default_retval=order.ft_price)( trade=trade, # type: ignore[arg-type] order=order, pair=trade.pair, current_time=current_time, - proposed_rate=row[OPEN_IDX], current_order_rate=order.price, + proposed_rate=row[OPEN_IDX], current_order_rate=order.ft_price, entry_tag=trade.enter_tag, side=trade.trade_direction ) # default value is current order price # cancel existing order whenever a new rate is requested (or None) - if requested_rate == order.price: + if requested_rate == order.ft_price: # assumption: there can't be multiple open entry orders at any given time return False else: @@ -1025,8 +1030,12 @@ class Backtesting: if requested_rate: self._enter_trade(pair=trade.pair, row=row, trade=trade, requested_rate=requested_rate, - requested_stake=(order.remaining * order.price / trade.leverage), + requested_stake=( + order.safe_remaining * order.ft_price / trade.leverage), direction='short' if trade.is_short else 'long') + # Delete trade if no successful entries happened (if placing the new order failed) + if trade.open_order_id is None and trade.nr_of_successful_entries == 0: + return True self.replaced_entry_orders += 1 else: # assumption: there can't be multiple open entry orders at any given time @@ -1049,9 +1058,21 @@ class Backtesting: return None return row + def _collate_rejected(self, pair, row): + """ + Temporarily store rejected signal information for downstream use in backtesting_analysis + """ + # It could be fun to enable hyperopt mode to write + # a loss function to reduce rejected signals + if (self.config.get('export', 'none') == 'signals' and + self.dataprovider.runmode == RunMode.BACKTEST): + if pair not in self.rejected_dict: + self.rejected_dict[pair] = [] + self.rejected_dict[pair].append([row[DATE_IDX], row[ENTER_TAG_IDX]]) + def backtest_loop( self, row: Tuple, pair: str, current_time: datetime, end_date: datetime, - max_open_trades: int, open_trade_count_start: int, trade_dir: Optional[LongShort], + open_trade_count_start: int, trade_dir: Optional[LongShort], is_first: bool = True) -> int: """ NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized. @@ -1074,36 +1095,38 @@ class Backtesting: if ( (self._position_stacking or len(LocalTrade.bt_trades_open_pp[pair]) == 0) and is_first - and self.trade_slot_available(max_open_trades, open_trade_count_start) and current_time != end_date and trade_dir is not None and not PairLocks.is_pair_locked(pair, row[DATE_IDX], trade_dir) ): - trade = self._enter_trade(pair, row, trade_dir) - if trade: - # TODO: hacky workaround to avoid opening > max_open_trades - # This emulates previous behavior - not sure if this is correct - # Prevents entering if the trade-slot was freed in this candle - open_trade_count_start += 1 - # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") - LocalTrade.add_bt_trade(trade) - self.wallets.update() + if (self.trade_slot_available(open_trade_count_start)): + trade = self._enter_trade(pair, row, trade_dir) + if trade: + # TODO: hacky workaround to avoid opening > max_open_trades + # This emulates previous behavior - not sure if this is correct + # Prevents entering if the trade-slot was freed in this candle + open_trade_count_start += 1 + # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") + LocalTrade.add_bt_trade(trade) + self.wallets.update() + else: + self._collate_rejected(pair, row) for trade in list(LocalTrade.bt_trades_open_pp[pair]): # 3. Process entry orders. order = trade.select_order(trade.entry_side, is_open=True) - if order and self._get_order_filled(order.price, row): + if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_time, trade) trade.open_order_id = None self.wallets.update() # 4. Create exit orders (if any) if not trade.open_order_id: - self._get_exit_trade_entry(trade, row, is_first) # Place exit order if necessary + self._check_trade_exit(trade, row) # Place exit order if necessary # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) - if order and self._get_order_filled(order.price, row): + if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_time, trade) trade.open_order_id = None sub_trade = order.safe_amount_after_fee != trade.amount @@ -1112,7 +1135,7 @@ class Backtesting: trade.recalc_trade_from_orders() else: trade.close_date = current_time - trade.close(order.price, show_msg=False) + trade.close(order.ft_price, show_msg=False) # logger.debug(f"{pair} - Backtesting exit {trade}") LocalTrade.close_bt_trade(trade) @@ -1121,8 +1144,7 @@ class Backtesting: return open_trade_count_start def backtest(self, processed: Dict, - start_date: datetime, end_date: datetime, - max_open_trades: int = 0) -> Dict[str, Any]: + start_date: datetime, end_date: datetime) -> Dict[str, Any]: """ Implement backtesting functionality @@ -1134,7 +1156,6 @@ class Backtesting: optimize memory usage! :param start_date: backtesting timerange start datetime :param end_date: backtesting timerange end datetime - :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited :return: DataFrame with trades (results of backtesting) """ self.prepare_backtest(self.enable_protections) @@ -1154,6 +1175,8 @@ class Backtesting: while current_time <= end_date: open_trade_count_start = LocalTrade.bt_open_open_trade_count self.check_abort() + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( + current_time=current_time) for i, pair in enumerate(data): row_index = indexes[pair] row = self.validate_row(data, pair, row_index, current_time) @@ -1183,7 +1206,7 @@ class Backtesting: if len(detail_data) == 0: # Fall back to "regular" data if no detail data was found for this candle open_trade_count_start = self.backtest_loop( - row, pair, current_time, end_date, max_open_trades, + row, pair, current_time, end_date, open_trade_count_start, trade_dir) continue detail_data.loc[:, 'enter_long'] = row[LONG_IDX] @@ -1196,13 +1219,13 @@ class Backtesting: current_time_det = current_time for det_row in detail_data[HEADERS].values.tolist(): open_trade_count_start = self.backtest_loop( - det_row, pair, current_time_det, end_date, max_open_trades, + det_row, pair, current_time_det, end_date, open_trade_count_start, trade_dir, is_first) current_time_det += timedelta(minutes=self.timeframe_detail_min) is_first = False else: open_trade_count_start = self.backtest_loop( - row, pair, current_time, end_date, max_open_trades, + row, pair, current_time, end_date, open_trade_count_start, trade_dir) # Move time one configured time_interval ahead. @@ -1229,19 +1252,17 @@ class Backtesting: def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame], timerange: TimeRange): self.progress.init_step(BacktestState.ANALYZE, 0) - - logger.info(f"Running backtesting for Strategy {strat.get_strategy_name()}") + strategy_name = strat.get_strategy_name() + logger.info(f"Running backtesting for Strategy {strategy_name}") backtest_start_time = datetime.now(timezone.utc) self._set_strategy(strat) # Use max_open_trades in backtesting, except --disable-max-market-positions is set - if self.config.get('use_max_market_positions', True): - # Must come from strategy config, as the strategy may modify this setting. - max_open_trades = self.strategy.config['max_open_trades'] - else: + if not self.config.get('use_max_market_positions', True): logger.info( 'Ignoring max_open_trades (--disable-max-market-positions was used) ...') - max_open_trades = 0 + self.strategy.max_open_trades = float('inf') + self.config.update({'max_open_trades': self.strategy.max_open_trades}) # need to reprocess data every time to populate signals preprocessed = self.strategy.advise_all_indicators(data) @@ -1264,41 +1285,24 @@ class Backtesting: processed=preprocessed, start_date=min_date, end_date=max_date, - max_open_trades=max_open_trades, ) backtest_end_time = datetime.now(timezone.utc) results.update({ - 'run_id': self.run_ids.get(strat.get_strategy_name(), ''), + 'run_id': self.run_ids.get(strategy_name, ''), 'backtest_start_time': int(backtest_start_time.timestamp()), 'backtest_end_time': int(backtest_end_time.timestamp()), }) - self.all_results[self.strategy.get_strategy_name()] = results + self.all_results[strategy_name] = results if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): - self._generate_trade_signal_candles(preprocessed_tmp, results) + self.processed_dfs[strategy_name] = generate_trade_signal_candles( + preprocessed_tmp, results) + self.rejected_df[strategy_name] = generate_rejected_signals( + preprocessed_tmp, self.rejected_dict) return min_date, max_date - def _generate_trade_signal_candles(self, preprocessed_df, bt_results): - signal_candles_only = {} - for pair in preprocessed_df.keys(): - signal_candles_only_df = DataFrame() - - pairdf = preprocessed_df[pair] - resdf = bt_results['results'] - pairresults = resdf.loc[(resdf["pair"] == pair)] - - if pairdf.shape[0] > 0: - for t, v in pairresults.open_date.items(): - allinds = pairdf.loc[(pairdf['date'] < v)] - signal_inds = allinds.iloc[[-1]] - signal_candles_only_df = pd.concat([signal_candles_only_df, signal_inds]) - - signal_candles_only[pair] = signal_candles_only_df - - self.processed_dfs[self.strategy.get_strategy_name()] = signal_candles_only - def _get_min_cached_backtest_date(self): min_backtest_date = None backtest_cache_age = self.config.get('backtest_cache', constants.BACKTEST_CACHE_DEFAULT) @@ -1361,8 +1365,9 @@ class Backtesting: if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): - store_backtest_signal_candles( - self.config['exportfilename'], self.processed_dfs, dt_appendix) + store_backtest_analysis_results( + self.config['exportfilename'], self.processed_dfs, self.rejected_df, + dt_appendix) # Results may be mixed up now. Sort them so they follow --strategy-list order. if 'strategy_list' in self.config and len(self.results) > 0: diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 2eb1c53f5..07c54d720 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -32,7 +32,7 @@ class EdgeCli: # Ensure using dry-run self.config['dry_run'] = True self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) + self.exchange = ExchangeResolver.load_exchange(self.config) self.strategy = StrategyResolver.load_strategy(self.config) self.strategy.dp = DataProvider(config, self.exchange) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b459d59f2..fe590f0d2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -13,13 +13,13 @@ from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional, Tuple -import progressbar import rapidjson -from colorama import Fore, Style from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib.externals import cloudpickle from pandas import DataFrame +from rich.progress import (BarColumn, MofNCompleteColumn, Progress, TaskProgressColumn, TextColumn, + TimeElapsedColumn, TimeRemainingColumn) from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.data.converter import trim_dataframes @@ -44,8 +44,6 @@ with warnings.catch_warnings(): from skopt import Optimizer from skopt.space import Dimension -progressbar.streams.wrap_stderr() -progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) @@ -74,6 +72,7 @@ class Hyperopt: self.roi_space: List[Dimension] = [] self.stoploss_space: List[Dimension] = [] self.trailing_space: List[Dimension] = [] + self.max_open_trades_space: List[Dimension] = [] self.dimensions: List[Dimension] = [] self.config = config @@ -117,11 +116,10 @@ class Hyperopt: self.current_best_epoch: Optional[Dict[str, Any]] = None # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set - if self.config.get('use_max_market_positions', True): - self.max_open_trades = self.config['max_open_trades'] - else: + if not self.config.get('use_max_market_positions', True): logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') - self.max_open_trades = 0 + self.backtesting.strategy.max_open_trades = float('inf') + config.update({'max_open_trades': self.backtesting.strategy.max_open_trades}) if HyperoptTools.has_space(self.config, 'sell'): # Make sure use_exit_signal is enabled @@ -209,6 +207,10 @@ class Hyperopt: result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} if HyperoptTools.has_space(self.config, 'trailing'): result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) + if HyperoptTools.has_space(self.config, 'trades'): + result['max_open_trades'] = { + 'max_open_trades': self.backtesting.strategy.max_open_trades + if self.backtesting.strategy.max_open_trades != float('inf') else -1} return result @@ -229,6 +231,8 @@ class Hyperopt: 'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset, 'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached, } + if not HyperoptTools.has_space(self.config, 'trades'): + result['max_open_trades'] = {'max_open_trades': strategy.max_open_trades} return result def print_results(self, results) -> None: @@ -280,8 +284,13 @@ class Hyperopt: logger.debug("Hyperopt has 'trailing' space") self.trailing_space = self.custom_hyperopt.trailing_space() + if HyperoptTools.has_space(self.config, 'trades'): + logger.debug("Hyperopt has 'trades' space") + self.max_open_trades_space = self.custom_hyperopt.max_open_trades_space() + self.dimensions = (self.buy_space + self.sell_space + self.protection_space - + self.roi_space + self.stoploss_space + self.trailing_space) + + self.roi_space + self.stoploss_space + self.trailing_space + + self.max_open_trades_space) def assign_params(self, params_dict: Dict, category: str) -> None: """ @@ -328,6 +337,20 @@ class Hyperopt: self.backtesting.strategy.trailing_only_offset_is_reached = \ d['trailing_only_offset_is_reached'] + if HyperoptTools.has_space(self.config, 'trades'): + if self.config["stake_amount"] == "unlimited" and \ + (params_dict['max_open_trades'] == -1 or params_dict['max_open_trades'] == 0): + # Ignore unlimited max open trades if stake amount is unlimited + params_dict.update({'max_open_trades': self.config['max_open_trades']}) + + updated_max_open_trades = int(params_dict['max_open_trades']) \ + if (params_dict['max_open_trades'] != -1 + and params_dict['max_open_trades'] != 0) else float('inf') + + self.config.update({'max_open_trades': updated_max_open_trades}) + + self.backtesting.strategy.max_open_trades = updated_max_open_trades + with self.data_pickle_file.open('rb') as f: processed = load(f, mmap_mode='r') if self.analyze_per_epoch: @@ -337,8 +360,7 @@ class Hyperopt: bt_results = self.backtesting.backtest( processed=processed, start_date=self.min_date, - end_date=self.max_date, - max_open_trades=self.max_open_trades, + end_date=self.max_date ) backtest_end_time = datetime.now(timezone.utc) bt_results.update({ @@ -357,7 +379,8 @@ class Hyperopt: strat_stats = generate_strategy_stats( self.pairlist, self.backtesting.strategy.get_strategy_name(), - backtesting_results, min_date, max_date, market_change=self.market_change + backtesting_results, min_date, max_date, market_change=self.market_change, + is_hyperopt=True, ) results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) @@ -496,29 +519,6 @@ class Hyperopt: else: return self.opt.ask(n_points=n_points), [False for _ in range(n_points)] - def get_progressbar_widgets(self): - if self.print_colorized: - widgets = [ - ' [Epoch ', progressbar.Counter(), ' of ', str(self.total_epochs), - ' (', progressbar.Percentage(), ')] ', - progressbar.Bar(marker=progressbar.AnimatedMarker( - fill='\N{FULL BLOCK}', - fill_wrap=Fore.GREEN + '{}' + Fore.RESET, - marker_wrap=Style.BRIGHT + '{}' + Style.RESET_ALL, - )), - ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', - ] - else: - widgets = [ - ' [Epoch ', progressbar.Counter(), ' of ', str(self.total_epochs), - ' (', progressbar.Percentage(), ')] ', - progressbar.Bar(marker=progressbar.AnimatedMarker( - fill='\N{FULL BLOCK}', - )), - ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', - ] - return widgets - def evaluate_result(self, val: Dict[str, Any], current: int, is_random: bool): """ Evaluate results returned from generate_optimizer @@ -578,11 +578,19 @@ class Hyperopt: logger.info(f'Effective number of parallel workers used: {jobs}') # Define progressbar - widgets = self.get_progressbar_widgets() - with progressbar.ProgressBar( - max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False, - widgets=widgets + with Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(bar_width=None), + MofNCompleteColumn(), + TaskProgressColumn(), + "•", + TimeElapsedColumn(), + "•", + TimeRemainingColumn(), + expand=True, ) as pbar: + task = pbar.add_task("Epochs", total=self.total_epochs) + start = 0 if self.analyze_per_epoch: @@ -592,7 +600,7 @@ class Hyperopt: f_val0 = self.generate_optimizer(asked[0]) self.opt.tell(asked, [f_val0['loss']]) self.evaluate_result(f_val0, 1, is_random[0]) - pbar.update(1) + pbar.update(task, advance=1) start += 1 evals = ceil((self.total_epochs - start) / jobs) @@ -606,14 +614,12 @@ class Hyperopt: f_val = self.run_optimizer_parallel(parallel, asked) self.opt.tell(asked, [v['loss'] for v in f_val]) - # Calculate progressbar outputs for j, val in enumerate(f_val): # Use human-friendly indexes here (starting from 1) current = i * jobs + j + 1 + start self.evaluate_result(val, current, is_random[j]) - - pbar.update(current) + pbar.update(task, advance=1) except KeyboardInterrupt: print('User interrupted..') diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 5bc0af42b..13c036a28 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -91,5 +91,8 @@ class HyperOptAuto(IHyperOpt): def trailing_space(self) -> List['Dimension']: return self._get_func('trailing_space')() + def max_open_trades_space(self) -> List['Dimension']: + return self._get_func('max_open_trades_space')() + def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType: return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index a7c64ffb0..65dd7ed87 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -191,6 +191,16 @@ class IHyperOpt(ABC): Categorical([True, False], name='trailing_only_offset_is_reached'), ] + def max_open_trades_space(self) -> List[Dimension]: + """ + Create a max open trades space. + + You may override it in your custom Hyperopt class. + """ + return [ + Integer(-1, 10, name='max_open_trades'), + ] + # This is needed for proper unpickling the class attribute timeframe # which is set to the actual value by the resolver. # Why do I still need such shamanic mantras in modern python? diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py index 9520123ee..88c97989a 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sharpe_daily.py @@ -44,7 +44,7 @@ class SharpeHyperOptLossDaily(IHyperOptLoss): sum_daily = ( results.resample(resample_freq, on='close_date').agg( - {"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0) + {"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0) ) total_profit = sum_daily["profit_ratio_after_slippage"] - risk_free_rate diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py index fac96664d..f5fe4590e 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_sortino_daily.py @@ -46,7 +46,7 @@ class SortinoHyperOptLossDaily(IHyperOptLoss): sum_daily = ( results.resample(resample_freq, on='close_date').agg( - {"profit_ratio_after_slippage": sum}).reindex(t_index).fillna(0) + {"profit_ratio_after_slippage": 'sum'}).reindex(t_index).fillna(0) ) total_profit = sum_daily["profit_ratio_after_slippage"] - minimum_acceptable_return diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py old mode 100755 new mode 100644 index 7007ec55e..1e7befdf6 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -1,4 +1,3 @@ -import io import logging from copy import deepcopy from datetime import datetime, timezone @@ -24,6 +23,8 @@ logger = logging.getLogger(__name__) NON_OPT_PARAM_APPENDIX = " # value loaded from strategy" +HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN + def hyperopt_serializer(x): if isinstance(x, np.integer): @@ -77,9 +78,18 @@ class HyperoptTools(): with filename.open('w') as f: rapidjson.dump(final_params, f, indent=2, default=hyperopt_serializer, - number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN + number_mode=HYPER_PARAMS_FILE_FORMAT ) + @staticmethod + def load_params(filename: Path) -> Dict: + """ + Load parameters from file + """ + with filename.open('r') as f: + params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT) + return params + @staticmethod def try_export_params(config: Config, strategy_name: str, params: Dict): if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False): @@ -96,7 +106,7 @@ class HyperoptTools(): Tell if the space value is contained in the configuration """ # 'trailing' and 'protection spaces are not included in the 'default' set of spaces - if space in ('trailing', 'protection'): + if space in ('trailing', 'protection', 'trades'): return any(s in config['spaces'] for s in [space, 'all']) else: return any(s in config['spaces'] for s in [space, 'all', 'default']) @@ -170,7 +180,7 @@ class HyperoptTools(): @staticmethod def show_epoch_details(results, total_epochs: int, print_json: bool, - no_header: bool = False, header_str: str = None) -> None: + no_header: bool = False, header_str: Optional[str] = None) -> None: """ Display details of the hyperopt result """ @@ -187,9 +197,10 @@ class HyperoptTools(): if print_json: result_dict: Dict = {} - for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']: + for s in ['buy', 'sell', 'protection', + 'roi', 'stoploss', 'trailing', 'max_open_trades']: HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s) - print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) + print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT)) else: HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:", @@ -201,6 +212,8 @@ class HyperoptTools(): HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized) HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized) HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized) + HyperoptTools._params_pretty_print( + params, 'max_open_trades', "Max Open Trades:", non_optimized) @staticmethod def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None: @@ -239,7 +252,9 @@ class HyperoptTools(): if space == "stoploss": stoploss = safe_value_fallback2(space_params, no_params, space, space) result += (f"stoploss = {stoploss}{appendix}") - + elif space == "max_open_trades": + max_open_trades = safe_value_fallback2(space_params, no_params, space, space) + result += (f"max_open_trades = {max_open_trades}{appendix}") elif space == "roi": result = result[:-1] + f'{appendix}\n' minimal_roi_result = rapidjson.dumps({ @@ -259,7 +274,7 @@ class HyperoptTools(): print(result) @staticmethod - def _space_params(params, space: str, r: int = None) -> Dict: + def _space_params(params, space: str, r: Optional[int] = None) -> Dict: d = params.get(space) if d: # Round floats to `r` digits after the decimal point if requested @@ -459,8 +474,8 @@ class HyperoptTools(): return try: - io.open(csv_file, 'w+').close() - except IOError: + Path(csv_file).open('w+').close() + except OSError: logger.error(f"Failed to create CSV file: {csv_file}") return diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 7de8f1a47..e60047a79 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -4,11 +4,11 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List, Union -from pandas import DataFrame, to_datetime +from pandas import DataFrame, concat, to_datetime from tabulate import tabulate -from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT, - Config) +from freqtrade.constants import (BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, + UNLIMITED_STAKE_AMOUNT, Config, IntOrInf) from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, calculate_expectancy, calculate_market_change, calculate_max_drawdown, calculate_sharpe, calculate_sortino) @@ -46,29 +46,80 @@ def store_backtest_stats( file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) -def store_backtest_signal_candles( - recordfilename: Path, candles: Dict[str, Dict], dtappendix: str) -> Path: +def _store_backtest_analysis_data( + recordfilename: Path, data: Dict[str, Dict], + dtappendix: str, name: str) -> Path: """ - Stores backtest trade signal candles + Stores backtest trade candles for analysis :param recordfilename: Path object, which can either be a filename or a directory. Filenames will be appended with a timestamp right before the suffix - while for directories, /backtest-result-_signals.pkl will be used + while for directories, /backtest-result-_.pkl will be used as filename - :param stats: Dict containing the backtesting signal candles + :param candles: Dict containing the backtesting data for analysis :param dtappendix: Datetime to use for the filename + :param name: Name to use for the file, e.g. signals, rejected """ if recordfilename.is_dir(): - filename = (recordfilename / f'backtest-result-{dtappendix}_signals.pkl') + filename = (recordfilename / f'backtest-result-{dtappendix}_{name}.pkl') else: filename = Path.joinpath( - recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_signals.pkl' + recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_{name}.pkl' ) - file_dump_joblib(filename, candles) + file_dump_joblib(filename, data) return filename +def store_backtest_analysis_results( + recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict], + dtappendix: str) -> None: + _store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals") + _store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected") + + +def generate_trade_signal_candles(preprocessed_df: Dict[str, DataFrame], + bt_results: Dict[str, Any]) -> DataFrame: + signal_candles_only = {} + for pair in preprocessed_df.keys(): + signal_candles_only_df = DataFrame() + + pairdf = preprocessed_df[pair] + resdf = bt_results['results'] + pairresults = resdf.loc[(resdf["pair"] == pair)] + + if pairdf.shape[0] > 0: + for t, v in pairresults.open_date.items(): + allinds = pairdf.loc[(pairdf['date'] < v)] + signal_inds = allinds.iloc[[-1]] + signal_candles_only_df = concat([ + signal_candles_only_df.infer_objects(), + signal_inds.infer_objects()]) + + signal_candles_only[pair] = signal_candles_only_df + return signal_candles_only + + +def generate_rejected_signals(preprocessed_df: Dict[str, DataFrame], + rejected_dict: Dict[str, DataFrame]) -> Dict[str, DataFrame]: + rejected_candles_only = {} + for pair, signals in rejected_dict.items(): + rejected_signals_only_df = DataFrame() + pairdf = preprocessed_df[pair] + + for t in signals: + data_df_row = pairdf.loc[(pairdf['date'] == t[0])].copy() + data_df_row['pair'] = pair + data_df_row['enter_tag'] = t[1] + + rejected_signals_only_df = concat([ + rejected_signals_only_df.infer_objects(), + data_df_row.infer_objects()]) + + rejected_candles_only[pair] = rejected_signals_only_df + return rejected_candles_only + + def _get_line_floatfmt(stake_currency: str) -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) @@ -191,7 +242,7 @@ def generate_tag_metrics(tag_type: str, return [] -def generate_exit_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: +def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results :param max_open_trades: Max_open_trades parameter @@ -273,7 +324,8 @@ def _get_resample_from_period(period: str) -> str: if period == 'day': return '1d' if period == 'week': - return '1w' + # Weekly defaulting to Monday. + return '1W-MON' if period == 'month': return '1M' raise ValueError(f"Period {period} is not supported.") @@ -295,6 +347,7 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic stats.append( { 'date': name.strftime('%d/%m/%Y'), + 'date_ts': int(name.to_pydatetime().timestamp() * 1000), 'profit_abs': profit_abs, 'wins': wins, 'draws': draws, @@ -304,6 +357,13 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic return stats +def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]: + result = {} + for period in BACKTEST_BREAKDOWNS: + result[period] = generate_periodic_breakdown_stats(trade_list, period) + return result + + def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: """ Generate overall trade statistics """ if len(results) == 0: @@ -380,7 +440,8 @@ def generate_strategy_stats(pairlist: List[str], strategy: str, content: Dict[str, Any], min_date: datetime, max_date: datetime, - market_change: float + market_change: float, + is_hyperopt: bool = False, ) -> Dict[str, Any]: """ :param pairlist: List of pairs to backtest @@ -415,6 +476,11 @@ def generate_strategy_stats(pairlist: List[str], daily_stats = generate_daily_stats(results) trade_stats = generate_trading_stats(results) + + periodic_breakdown = {} + if not is_hyperopt: + periodic_breakdown = {'periodic_breakdown': generate_all_periodic_breakdown_stats(results)} + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], @@ -433,7 +499,6 @@ def generate_strategy_stats(pairlist: List[str], 'results_per_enter_tag': enter_tag_results, 'exit_reason_summary': exit_reason_stats, 'left_open_trades': left_open_results, - # 'days_breakdown_stats': days_breakdown_stats, 'total_trades': len(results), 'trade_count_long': len(results.loc[~results['is_short']]), @@ -498,6 +563,7 @@ def generate_strategy_stats(pairlist: List[str], 'exit_profit_only': config['exit_profit_only'], 'exit_profit_offset': config['exit_profit_offset'], 'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'], + **periodic_breakdown, **daily_stats, **trade_stats } @@ -865,6 +931,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) + table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + if (results.get('results_per_enter_tag') is not None or results.get('results_per_buy_tag') is not None): # results_per_buy_tag is deprecated and should be removed 2 versions after short golive. @@ -884,14 +955,12 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - for period in backtest_breakdown: - days_breakdown_stats = generate_periodic_breakdown_stats( - trade_list=results['trades'], period=period) + if period in results.get('periodic_breakdown', {}): + days_breakdown_stats = results['periodic_breakdown'][period] + else: + days_breakdown_stats = generate_periodic_breakdown_stats( + trade_list=results['trades'], period=period) table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats, stake_currency=stake_currency, period=period) if isinstance(table, str) and len(table) > 0: @@ -917,11 +986,11 @@ def show_backtest_results(config: Config, backtest_stats: Dict): strategy, results, stake_currency, config.get('backtest_breakdown', [])) - if len(backtest_stats['strategy']) > 1: + if len(backtest_stats['strategy']) > 0: # Print Strategy summary table table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency) - print(f"{results['backtest_start']} -> {results['backtest_end']} |" + print(f"Backtested {results['backtest_start']} -> {results['backtest_end']} |" f" Max open trades : {results['max_open_trades']}") print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py index bbdac4ab9..6c59a4d8f 100644 --- a/freqtrade/optimize/space/__init__.py +++ b/freqtrade/optimize/space/__init__.py @@ -1,4 +1,3 @@ -# flake8: noqa: F401 -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer, Real # noqa: F401 -from .decimalspace import SKDecimal +from .decimalspace import SKDecimal # noqa: F401 diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 9e1a7e922..4cf7aa455 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/persistence/base.py b/freqtrade/persistence/base.py index fb2d561e1..fc2dac75e 100644 --- a/freqtrade/persistence/base.py +++ b/freqtrade/persistence/base.py @@ -1,7 +1,9 @@ -from typing import Any - -from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import DeclarativeBase, Session, scoped_session -_DECL_BASE: Any = declarative_base() +SessionType = scoped_session[Session] + + +class ModelBase(DeclarativeBase): + pass diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py new file mode 100644 index 000000000..110a23d6c --- /dev/null +++ b/freqtrade/persistence/key_value_store.py @@ -0,0 +1,179 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import ClassVar, Optional, Union + +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from freqtrade.persistence.base import ModelBase, SessionType + + +ValueTypes = Union[str, datetime, float, int] + + +class ValueTypesEnum(str, Enum): + STRING = 'str' + DATETIME = 'datetime' + FLOAT = 'float' + INT = 'int' + + +class KeyStoreKeys(str, Enum): + BOT_START_TIME = 'bot_start_time' + STARTUP_TIME = 'startup_time' + + +class _KeyValueStoreModel(ModelBase): + """ + Pair Locks database model. + """ + __tablename__ = 'KeyValueStore' + session: ClassVar[SessionType] + + id: Mapped[int] = mapped_column(primary_key=True) + + key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True) + + value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False) + + string_value: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + datetime_value: Mapped[Optional[datetime]] + float_value: Mapped[Optional[float]] + int_value: Mapped[Optional[int]] + + +class KeyValueStore(): + """ + Generic bot-wide, persistent key-value store + Can be used to store generic values, e.g. very first bot startup time. + Supports the types str, datetime, float and int. + """ + + @staticmethod + def store_value(key: KeyStoreKeys, value: ValueTypes) -> None: + """ + Store the given value for the given key. + :param key: Key to store the value for - can be used in get-value to retrieve the key + :param value: Value to store - can be str, datetime, float or int + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + kv = _KeyValueStoreModel(key=key) + if isinstance(value, str): + kv.value_type = ValueTypesEnum.STRING + kv.string_value = value + elif isinstance(value, datetime): + kv.value_type = ValueTypesEnum.DATETIME + kv.datetime_value = value + elif isinstance(value, float): + kv.value_type = ValueTypesEnum.FLOAT + kv.float_value = value + elif isinstance(value, int): + kv.value_type = ValueTypesEnum.INT + kv.int_value = value + else: + raise ValueError(f'Unknown value type {kv.value_type}') + _KeyValueStoreModel.session.add(kv) + _KeyValueStoreModel.session.commit() + + @staticmethod + def delete_value(key: KeyStoreKeys) -> None: + """ + Delete the value for the given key. + :param key: Key to delete the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is not None: + _KeyValueStoreModel.session.delete(kv) + _KeyValueStoreModel.session.commit() + + @staticmethod + def get_value(key: KeyStoreKeys) -> Optional[ValueTypes]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + return None + if kv.value_type == ValueTypesEnum.STRING: + return kv.string_value + if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: + return kv.datetime_value.replace(tzinfo=timezone.utc) + if kv.value_type == ValueTypesEnum.FLOAT: + return kv.float_value + if kv.value_type == ValueTypesEnum.INT: + return kv.int_value + # This should never happen unless someone messed with the database manually + raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover + + @staticmethod + def get_string_value(key: KeyStoreKeys) -> Optional[str]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.STRING).first() + if kv is None: + return None + return kv.string_value + + @staticmethod + def get_datetime_value(key: KeyStoreKeys) -> Optional[datetime]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME).first() + if kv is None or kv.datetime_value is None: + return None + return kv.datetime_value.replace(tzinfo=timezone.utc) + + @staticmethod + def get_float_value(key: KeyStoreKeys) -> Optional[float]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT).first() + if kv is None: + return None + return kv.float_value + + @staticmethod + def get_int_value(key: KeyStoreKeys) -> Optional[int]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.INT).first() + if kv is None: + return None + return kv.int_value + + +def set_startup_time(): + """ + sets bot_start_time to the first trade open date - or "now" on new databases. + sets startup_time to "now" + """ + st = KeyValueStore.get_value('bot_start_time') + if st is None: + from freqtrade.persistence import Trade + t = Trade.session.query(Trade).order_by(Trade.open_date.asc()).first() + if t is not None: + KeyValueStore.store_value('bot_start_time', t.open_date_utc) + else: + KeyValueStore.store_value('bot_start_time', datetime.now(timezone.utc)) + KeyValueStore.store_value('startup_time', datetime.now(timezone.utc)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7f851322e..e561e727b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,6 +2,9 @@ This module contains the class to persist trades into SQLite """ import logging +import threading +from contextvars import ContextVar +from typing import Any, Dict, Final, Optional from sqlalchemy import create_engine, inspect from sqlalchemy.exc import NoSuchModuleError @@ -9,7 +12,8 @@ from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException -from freqtrade.persistence.base import _DECL_BASE +from freqtrade.persistence.base import ModelBase +from freqtrade.persistence.key_value_store import _KeyValueStoreModel from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade @@ -18,6 +22,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' @@ -29,7 +49,7 @@ def init_db(db_url: str) -> None: :param db_url: Database to use :return: None """ - kwargs = {} + kwargs: Dict[str, Any] = {} if db_url == 'sqlite:///': raise OperationalException( @@ -52,12 +72,13 @@ 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)) - Trade.query = Trade._session.query_property() - Order.query = Trade._session.query_property() - PairLock.query = Trade._session.query_property() + # 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 + _KeyValueStoreModel.session = Trade.session previous_tables = inspect(engine).get_table_names() - _DECL_BASE.metadata.create_all(engine) - check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) + ModelBase.metadata.create_all(engine) + check_migrate(engine, decl_base=ModelBase, previous_tables=previous_tables) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index 926c641b0..1b254c2b2 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -1,33 +1,34 @@ from datetime import datetime, timezone -from typing import Any, Dict, Optional +from typing import Any, ClassVar, Dict, Optional -from sqlalchemy import Boolean, Column, DateTime, Integer, String, or_ -from sqlalchemy.orm import Query +from sqlalchemy import ScalarResult, String, or_, select +from sqlalchemy.orm import Mapped, mapped_column from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.persistence.base import _DECL_BASE +from freqtrade.persistence.base import ModelBase, SessionType -class PairLock(_DECL_BASE): +class PairLock(ModelBase): """ Pair Locks database model. """ __tablename__ = 'pairlocks' + session: ClassVar[SessionType] - id = Column(Integer, primary_key=True) + id: Mapped[int] = mapped_column(primary_key=True) - pair = Column(String(25), nullable=False, index=True) + pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # lock direction - long, short or * (for both) - side = Column(String(25), nullable=False, default="*") - reason = Column(String(255), nullable=True) + side: Mapped[str] = mapped_column(String(25), nullable=False, default="*") + reason: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # Time the pair was locked (start time) - lock_time = Column(DateTime, nullable=False) + lock_time: Mapped[datetime] = mapped_column(nullable=False) # Time until the pair is locked (end time) - lock_end_time = Column(DateTime, nullable=False, index=True) + lock_end_time: Mapped[datetime] = mapped_column(nullable=False, index=True) - active = Column(Boolean, nullable=False, default=True, index=True) + active: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) - def __repr__(self): + def __repr__(self) -> str: lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT) return ( @@ -35,7 +36,8 @@ class PairLock(_DECL_BASE): f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})') @staticmethod - def query_pair_locks(pair: Optional[str], now: datetime, side: str = '*') -> Query: + def query_pair_locks( + pair: Optional[str], now: datetime, side: str = '*') -> ScalarResult['PairLock']: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -51,9 +53,11 @@ class PairLock(_DECL_BASE): else: filters.append(PairLock.side == '*') - return PairLock.query.filter( - *filters - ) + return PairLock.session.scalars(select(PairLock).filter(*filters)) + + @staticmethod + def get_all_locks() -> ScalarResult['PairLock']: + return PairLock.session.scalars(select(PairLock)) def to_json(self) -> Dict[str, Any]: return { diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 69d8b098b..29169a50d 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -1,6 +1,8 @@ import logging from datetime import datetime, timezone -from typing import List, Optional +from typing import List, Optional, Sequence + +from sqlalchemy import select from freqtrade.exchange import timeframe_to_next_date from freqtrade.persistence.models import PairLock @@ -30,8 +32,8 @@ class PairLocks(): PairLocks.locks = [] @staticmethod - def lock_pair(pair: str, until: datetime, reason: str = None, *, - now: datetime = None, side: str = '*') -> PairLock: + def lock_pair(pair: str, until: datetime, reason: Optional[str] = None, *, + now: Optional[datetime] = None, side: str = '*') -> PairLock: """ Create PairLock from now to "until". Uses database by default, unless PairLocks.use_db is set to False, @@ -51,15 +53,15 @@ class PairLocks(): active=True ) if PairLocks.use_db: - PairLock.query.session.add(lock) - PairLock.query.session.commit() + PairLock.session.add(lock) + PairLock.session.commit() else: PairLocks.locks.append(lock) return lock @staticmethod - def get_pair_locks( - pair: Optional[str], now: Optional[datetime] = None, side: str = '*') -> List[PairLock]: + def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None, + side: str = '*') -> Sequence[PairLock]: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -106,7 +108,7 @@ class PairLocks(): for lock in locks: lock.active = False if PairLocks.use_db: - PairLock.query.session.commit() + PairLock.session.commit() @staticmethod def unlock_reason(reason: str, now: Optional[datetime] = None) -> None: @@ -126,15 +128,15 @@ class PairLocks(): PairLock.active.is_(True), PairLock.reason == reason ] - locks = PairLock.query.filter(*filters) + locks = PairLock.session.scalars(select(PairLock).filter(*filters)).all() for lock in locks: logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.") lock.active = False - PairLock.query.session.commit() + PairLock.session.commit() else: # used in backtesting mode; don't show log messages for speed - locks = PairLocks.get_pair_locks(None) - for lock in locks: + locksb = PairLocks.get_pair_locks(None) + for lock in locksb: if lock.reason == reason: lock.active = False @@ -165,11 +167,11 @@ class PairLocks(): ) @staticmethod - def get_all_locks() -> List[PairLock]: + def get_all_locks() -> Sequence[PairLock]: """ Return all locks, also locks with expired end date """ if PairLocks.use_db: - return PairLock.query.all() + return PairLock.get_all_locks().all() else: return PairLocks.locks diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 3013df2b8..5d8aada6b 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -5,26 +5,27 @@ import logging from collections import defaultdict from datetime import datetime, timedelta, timezone from math import isclose -from typing import Any, Dict, List, Optional +from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast -from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, - UniqueConstraint, desc, func) -from sqlalchemy.orm import Query, lazyload, relationship +from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String, + UniqueConstraint, desc, func, select) +from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates -from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, - BuySell, LongShort) +from freqtrade.constants import (CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, + NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.exchange import amount_to_contract_precision, price_to_precision +from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, + price_to_precision) from freqtrade.leverage import interest -from freqtrade.persistence.base import _DECL_BASE -from freqtrade.util import FtPrecise +from freqtrade.persistence.base import ModelBase, SessionType +from freqtrade.util import FtPrecise, dt_now logger = logging.getLogger(__name__) -class Order(_DECL_BASE): +class Order(ModelBase): """ Order database model Keeps a record of all orders placed on the exchange @@ -36,41 +37,43 @@ class Order(_DECL_BASE): Mirrors CCXT Order structure """ __tablename__ = 'orders' + session: ClassVar[SessionType] + # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),) - id = Column(Integer, primary_key=True) - ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True) + ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True) - trade = relationship("Trade", back_populates="orders") + trade: Mapped[List["Trade"]] = relationship("Trade", back_populates="orders") # order_side can only be 'buy', 'sell' or 'stoploss' - ft_order_side: str = Column(String(25), nullable=False) - ft_pair: str = Column(String(25), nullable=False) - ft_is_open = Column(Boolean, nullable=False, default=True, index=True) - ft_amount = Column(Float, nullable=False) - ft_price = Column(Float, nullable=False) + ft_order_side: Mapped[str] = mapped_column(String(25), nullable=False) + ft_pair: Mapped[str] = mapped_column(String(25), nullable=False) + ft_is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) + ft_amount: Mapped[float] = mapped_column(Float(), nullable=False) + ft_price: Mapped[float] = mapped_column(Float(), nullable=False) - order_id: str = Column(String(255), nullable=False, index=True) - status = Column(String(255), nullable=True) - symbol = Column(String(25), nullable=True) - order_type: str = Column(String(50), nullable=True) - side = Column(String(25), nullable=True) - price = Column(Float, nullable=True) - average = Column(Float, nullable=True) - amount = Column(Float, nullable=True) - filled = Column(Float, nullable=True) - remaining = Column(Float, nullable=True) - cost = Column(Float, nullable=True) - stop_price = Column(Float, nullable=True) - order_date = Column(DateTime, nullable=True, default=datetime.utcnow) - order_filled_date = Column(DateTime, nullable=True) - order_update_date = Column(DateTime, nullable=True) + order_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + symbol: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) + # TODO: type: order_type type is Optional[str] + order_type: Mapped[str] = mapped_column(String(50), nullable=True) + side: Mapped[str] = mapped_column(String(25), nullable=True) + price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + average: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + amount: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + filled: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + remaining: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + stop_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) + order_date: Mapped[datetime] = mapped_column(nullable=True, default=dt_now) + order_filled_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) + order_update_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) + funding_fee: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) - funding_fee = Column(Float, nullable=True) - - ft_fee_base = Column(Float, nullable=True) + ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) @property def order_date_utc(self) -> datetime: @@ -96,6 +99,10 @@ class Order(_DECL_BASE): def safe_filled(self) -> float: return self.filled if self.filled is not None else self.amount or 0.0 + @property + def safe_cost(self) -> float: + return self.cost or 0.0 + @property def safe_remaining(self) -> float: return ( @@ -113,8 +120,9 @@ class Order(_DECL_BASE): def __repr__(self): - return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' - f'side={self.side}, order_type={self.order_type}, status={self.status})') + return (f"Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, " + f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, " + f"order_type={self.order_type}, status={self.status})") def update_from_ccxt_object(self, order): """ @@ -146,12 +154,12 @@ class Order(_DECL_BASE): # Assign funding fee up to this point # (represents the funding fee since the last order) self.funding_fee = self.trade.funding_fees - if (order.get('filled', 0.0) or 0.0) > 0: + if (order.get('filled', 0.0) or 0.0) > 0 and not self.order_filled_date: self.order_filled_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc) - def to_ccxt_object(self) -> Dict[str, Any]: - return { + def to_ccxt_object(self, stopPriceName: str = 'stopPrice') -> Dict[str, Any]: + order: Dict[str, Any] = { 'id': self.order_id, 'symbol': self.ft_pair, 'price': self.price, @@ -162,17 +170,23 @@ class Order(_DECL_BASE): 'side': self.ft_order_side, 'filled': self.filled, 'remaining': self.remaining, - 'stopPrice': self.stop_price, 'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'), 'timestamp': int(self.order_date_utc.timestamp() * 1000), 'status': self.status, 'fee': None, 'info': {}, } + if self.ft_order_side == 'stoploss': + order.update({ + stopPriceName: self.stop_price, + 'ft_order_type': 'stoploss', + }) + + return order def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]: resp = { - 'amount': self.amount, + 'amount': self.safe_amount, 'safe_price': self.safe_price, 'ft_order_side': self.ft_order_side, 'order_filled_timestamp': int(self.order_filled_date.replace( @@ -210,7 +224,7 @@ class Order(_DECL_BASE): # Assumes backtesting will use date_last_filled_utc to calculate future funding fees. self.funding_fee = trade.funding_fees - if (self.ft_order_side == trade.entry_side): + if (self.ft_order_side == trade.entry_side and self.price): trade.open_rate = self.price trade.recalc_trade_from_orders() trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct, refresh=True) @@ -252,12 +266,12 @@ class Order(_DECL_BASE): return o @staticmethod - def get_open_orders() -> List['Order']: + def get_open_orders() -> Sequence['Order']: """ Retrieve open orders from the database :return: List of open orders """ - return Order.query.filter(Order.ft_is_open.is_(True)).all() + return Order.session.scalars(select(Order).filter(Order.ft_is_open.is_(True))).all() @staticmethod def order_by_id(order_id: str) -> Optional['Order']: @@ -265,7 +279,7 @@ class Order(_DECL_BASE): Retrieve order based on order_id :return: Order or None """ - return Order.query.filter(Order.order_id == order_id).first() + return Order.session.scalars(select(Order).filter(Order.order_id == order_id)).first() class LocalTrade(): @@ -290,15 +304,15 @@ class LocalTrade(): exchange: str = '' pair: str = '' - base_currency: str = '' - stake_currency: str = '' + base_currency: Optional[str] = '' + stake_currency: Optional[str] = '' is_open: bool = True fee_open: float = 0.0 fee_open_cost: Optional[float] = None - fee_open_currency: str = '' - fee_close: float = 0.0 + fee_open_currency: Optional[str] = '' + fee_close: Optional[float] = 0.0 fee_close_cost: Optional[float] = None - fee_close_currency: str = '' + fee_close_currency: Optional[str] = '' open_rate: float = 0.0 open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value @@ -308,7 +322,7 @@ class LocalTrade(): close_profit: Optional[float] = None close_profit_abs: Optional[float] = None stake_amount: float = 0.0 - max_stake_amount: float = 0.0 + max_stake_amount: Optional[float] = 0.0 amount: float = 0.0 amount_requested: Optional[float] = None open_date: datetime @@ -317,9 +331,9 @@ class LocalTrade(): # absolute value of the stop loss stop_loss: float = 0.0 # percentage value of the stop loss - stop_loss_pct: float = 0.0 + stop_loss_pct: Optional[float] = 0.0 # absolute value of the initial stop loss - initial_stop_loss: float = 0.0 + initial_stop_loss: Optional[float] = 0.0 # percentage value of the initial stop loss initial_stop_loss_pct: Optional[float] = None # stoploss order id which is on exchange @@ -327,12 +341,12 @@ class LocalTrade(): # last update time of the stoploss order on exchange stoploss_last_update: Optional[datetime] = None # absolute value of the highest reached price - max_rate: float = 0.0 + max_rate: Optional[float] = None # Lowest price reached - min_rate: float = 0.0 - exit_reason: str = '' - exit_order_status: str = '' - strategy: str = '' + min_rate: Optional[float] = None + exit_reason: Optional[str] = '' + exit_order_status: Optional[str] = '' + strategy: Optional[str] = '' enter_tag: Optional[str] = None timeframe: Optional[int] = None @@ -411,7 +425,7 @@ class LocalTrade(): @property def close_date_utc(self): - return self.close_date.replace(tzinfo=timezone.utc) + return self.close_date.replace(tzinfo=timezone.utc) if self.close_date else None @property def entry_side(self) -> str: @@ -508,6 +522,8 @@ class LocalTrade(): 'close_timestamp': int(self.close_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, 'realized_profit': self.realized_profit or 0.0, + # Close-profit corresponds to relative realized_profit ratio + 'realized_profit_ratio': self.close_profit or None, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, 'close_profit': self.close_profit, # Deprecated @@ -548,6 +564,9 @@ class LocalTrade(): 'trading_mode': self.trading_mode, 'funding_fees': self.funding_fees, 'open_order_id': self.open_order_id, + 'amount_precision': self.amount_precision, + 'price_precision': self.price_precision, + 'precision_mode': self.precision_mode, 'orders': orders, } @@ -582,14 +601,15 @@ class LocalTrade(): """ Method used internally to set self.stop_loss. """ - stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode) + stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode, + rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) if not self.stop_loss: self.initial_stop_loss = stop_loss_norm self.stop_loss = stop_loss_norm self.stop_loss_pct = -1 * abs(percent) - def adjust_stop_loss(self, current_price: float, stoploss: float, + def adjust_stop_loss(self, current_price: float, stoploss: Optional[float], initial: bool = False, refresh: bool = False) -> None: """ This adjusts the stop loss to it's most recently observed setting @@ -598,7 +618,7 @@ class LocalTrade(): :param initial: Called to initiate stop_loss. Skips everything if self.stop_loss is already set. """ - if initial and not (self.stop_loss is None or self.stop_loss == 0): + if stoploss is None or (initial and not (self.stop_loss is None or self.stop_loss == 0)): # Don't modify if called with initial and nothing to do return refresh = True if refresh and self.nr_of_successful_entries == 1 else False @@ -613,7 +633,8 @@ class LocalTrade(): if self.initial_stop_loss_pct is None or refresh: self.__set_stop_loss(new_loss, stoploss) self.initial_stop_loss = price_to_precision( - new_loss, self.price_precision, self.precision_mode) + new_loss, self.price_precision, self.precision_mode, + rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) self.initial_stop_loss_pct = -1 * abs(stoploss) # evaluate if the stop loss needs to be updated @@ -637,7 +658,7 @@ class LocalTrade(): f"initial_stop_loss={self.initial_stop_loss:.8f}, " f"stop_loss={self.stop_loss:.8f}. " f"Trailing stoploss saved us: " - f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") + f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}.") def update_trade(self, order: Order) -> None: """ @@ -677,21 +698,27 @@ class LocalTrade(): else: logger.warning( f'Got different open_order_id {self.open_order_id} != {order.order_id}') - amount_tr = amount_to_contract_precision(self.amount, self.amount_precision, - self.precision_mode, self.contract_size) - if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC): - self.close(order.safe_price) - else: - self.recalc_trade_from_orders() - elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'): + + elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value if self.is_open: logger.info(f'{order.order_type.upper()} is hit for {self}.') - self.close(order.safe_price) else: raise ValueError(f'Unknown order type: {order.order_type}') + + if order.ft_order_side != self.entry_side: + amount_tr = amount_to_contract_precision(self.amount, self.amount_precision, + self.precision_mode, self.contract_size) + if ( + isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC) + or order.safe_amount_after_fee > amount_tr + ): + self.close(order.safe_price) + else: + self.recalc_trade_from_orders() + Trade.commit() def close(self, rate: float, *, show_msg: bool = True) -> None: @@ -789,17 +816,17 @@ class LocalTrade(): return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours) - def _calc_base_close(self, amount: FtPrecise, rate: float, fee: float) -> FtPrecise: + def _calc_base_close(self, amount: FtPrecise, rate: float, fee: Optional[float]) -> FtPrecise: close_trade = amount * FtPrecise(rate) - fees = close_trade * FtPrecise(fee) + fees = close_trade * FtPrecise(fee or 0.0) if self.is_short: return close_trade + fees else: return close_trade - fees - def calc_close_trade_value(self, rate: float, amount: float = None) -> float: + def calc_close_trade_value(self, rate: float, amount: Optional[float] = None) -> float: """ Calculate the Trade's close value including fees :param rate: rate to compare with. @@ -837,7 +864,8 @@ class LocalTrade(): raise OperationalException( f"{self.trading_mode.value} trading is not yet available using freqtrade") - def calc_profit(self, rate: float, amount: float = None, open_rate: float = None) -> float: + def calc_profit(self, rate: float, amount: Optional[float] = None, + open_rate: Optional[float] = None) -> float: """ Calculate the absolute profit in stake currency between Close and Open trade :param rate: close rate to compare with. @@ -858,7 +886,8 @@ class LocalTrade(): return float(f"{profit:.8f}") def calc_profit_ratio( - self, rate: float, amount: float = None, open_rate: float = None) -> float: + self, rate: float, amount: Optional[float] = None, + open_rate: Optional[float] = None) -> float: """ Calculates the profit as ratio (including fee). :param rate: rate to compare with. @@ -1054,13 +1083,18 @@ class LocalTrade(): return len(self.select_filled_orders('sell')) @property - def sell_reason(self) -> str: + def sell_reason(self) -> Optional[str]: """ DEPRECATED! Please use exit_reason instead.""" return self.exit_reason + @property + def safe_close_rate(self) -> float: + return self.close_rate or self.close_rate_requested or 0.0 + @staticmethod - def get_trades_proxy(*, pair: str = None, is_open: bool = None, - open_date: datetime = None, close_date: datetime = None, + def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, + open_date: Optional[datetime] = None, + close_date: Optional[datetime] = None, ) -> List['LocalTrade']: """ Helper function to query Trades. @@ -1068,6 +1102,11 @@ class LocalTrade(): In live mode, converts the filter to a database query and returns all rows In Backtest mode, uses filters on Trade.trades to get the result. + :param pair: Filter by pair + :param is_open: Filter by open/closed status + :param open_date: Filter by open_date (filters via trade.open_date > input) + :param close_date: Filter by close_date (filters via trade.close_date > input) + Will implicitly only return closed trades. :return: unsorted List[Trade] """ @@ -1118,7 +1157,7 @@ class LocalTrade(): @staticmethod def get_open_trades() -> List[Any]: """ - Query trades from persistence layer + Retrieve open trades """ return Trade.get_trades_proxy(is_open=True) @@ -1128,7 +1167,9 @@ class LocalTrade(): get open trade count """ if Trade.use_db: - return Trade.query.filter(Trade.is_open.is_(True)).count() + return Trade.session.execute( + select(func.count(Trade.id)).filter(Trade.is_open.is_(True)) + ).scalar_one() else: return LocalTrade.bt_open_open_trade_count @@ -1153,7 +1194,7 @@ class LocalTrade(): logger.info(f"New stoploss: {trade.stop_loss}.") -class Trade(_DECL_BASE, LocalTrade): +class Trade(ModelBase, LocalTrade): """ Trade database model. Also handles updating and querying trades @@ -1161,104 +1202,132 @@ class Trade(_DECL_BASE, LocalTrade): Note: Fields must be aligned with LocalTrade class """ __tablename__ = 'trades' + session: ClassVar[SessionType] use_db: bool = True - id = Column(Integer, primary_key=True) + id: Mapped[int] = mapped_column(Integer, primary_key=True) # type: ignore - orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan", - lazy="selectin", innerjoin=True) + orders: Mapped[List[Order]] = relationship( + "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", + innerjoin=True) # type: ignore - exchange = Column(String(25), nullable=False) - pair = Column(String(25), nullable=False, index=True) - base_currency = Column(String(25), nullable=True) - stake_currency = Column(String(25), nullable=True) - is_open = Column(Boolean, nullable=False, default=True, index=True) - fee_open = Column(Float, nullable=False, default=0.0) - fee_open_cost = Column(Float, nullable=True) - fee_open_currency = Column(String(25), nullable=True) - fee_close = Column(Float, nullable=False, default=0.0) - fee_close_cost = Column(Float, nullable=True) - fee_close_currency = Column(String(25), nullable=True) - open_rate: float = Column(Float) - open_rate_requested = Column(Float) + exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore + pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore + base_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore + stake_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore + is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) # type: ignore + fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) # type: ignore + fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + fee_open_currency: Mapped[Optional[str]] = mapped_column( + String(25), nullable=True) # type: ignore + fee_close: Mapped[Optional[float]] = mapped_column( + Float(), nullable=False, default=0.0) # type: ignore + fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + fee_close_currency: Mapped[Optional[str]] = mapped_column( + String(25), nullable=True) # type: ignore + open_rate: Mapped[float] = mapped_column(Float()) # type: ignore + open_rate_requested: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True) # type: ignore # open_trade_value - calculated via _calc_open_trade_value - open_trade_value = Column(Float) - close_rate: Optional[float] = Column(Float) - close_rate_requested = Column(Float) - realized_profit = Column(Float, default=0.0) - close_profit = Column(Float) - close_profit_abs = Column(Float) - stake_amount = Column(Float, nullable=False) - max_stake_amount = Column(Float) - amount = Column(Float) - amount_requested = Column(Float) - open_date = Column(DateTime, nullable=False, default=datetime.utcnow) - close_date = Column(DateTime) - open_order_id = Column(String(255)) + open_trade_value: Mapped[float] = mapped_column(Float(), nullable=True) # type: ignore + close_rate: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + close_rate_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + realized_profit: Mapped[float] = mapped_column( + Float(), default=0.0, nullable=True) # type: ignore + close_profit: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + close_profit_abs: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + stake_amount: Mapped[float] = mapped_column(Float(), nullable=False) # type: ignore + max_stake_amount: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + amount: Mapped[float] = mapped_column(Float()) # type: ignore + amount_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore + open_date: Mapped[datetime] = mapped_column( + nullable=False, default=datetime.utcnow) # type: ignore + close_date: Mapped[Optional[datetime]] = mapped_column() # type: ignore + open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # type: ignore # absolute value of the stop loss - stop_loss = Column(Float, nullable=True, default=0.0) + stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore # percentage value of the stop loss - stop_loss_pct = Column(Float, nullable=True) + stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore # absolute value of the initial stop loss - initial_stop_loss = Column(Float, nullable=True, default=0.0) + initial_stop_loss: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True, default=0.0) # type: ignore # percentage value of the initial stop loss - initial_stop_loss_pct = Column(Float, nullable=True) + initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True) # type: ignore # stoploss order id which is on exchange - stoploss_order_id = Column(String(255), nullable=True, index=True) + stoploss_order_id: Mapped[Optional[str]] = mapped_column( + String(255), nullable=True, index=True) # type: ignore # last update time of the stoploss order on exchange - stoploss_last_update = Column(DateTime, nullable=True) + stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore # absolute value of the highest reached price - max_rate = Column(Float, nullable=True, default=0.0) + max_rate: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True, default=0.0) # type: ignore # Lowest price reached - min_rate = Column(Float, nullable=True) - exit_reason = Column(String(100), nullable=True) - exit_order_status = Column(String(100), nullable=True) - strategy = Column(String(100), nullable=True) - enter_tag = Column(String(100), nullable=True) - timeframe = Column(Integer, nullable=True) + min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + exit_reason: Mapped[Optional[str]] = mapped_column( + String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore + exit_order_status: Mapped[Optional[str]] = mapped_column( + String(100), nullable=True) # type: ignore + strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore + enter_tag: Mapped[Optional[str]] = mapped_column( + String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore + timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore - trading_mode = Column(Enum(TradingMode), nullable=True) - amount_precision = Column(Float, nullable=True) - price_precision = Column(Float, nullable=True) - precision_mode = Column(Integer, nullable=True) - contract_size = Column(Float, nullable=True) + trading_mode: Mapped[TradingMode] = mapped_column( + Enum(TradingMode), nullable=True) # type: ignore + amount_precision: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True) # type: ignore + price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore + contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore # Leverage trading properties - leverage = Column(Float, nullable=True, default=1.0) - is_short = Column(Boolean, nullable=False, default=False) - liquidation_price = Column(Float, nullable=True) + leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0) # type: ignore + is_short: Mapped[bool] = mapped_column(nullable=False, default=False) # type: ignore + liquidation_price: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True) # type: ignore # Margin Trading Properties - interest_rate = Column(Float, nullable=False, default=0.0) + interest_rate: Mapped[float] = mapped_column( + Float(), nullable=False, default=0.0) # type: ignore # Futures properties - funding_fees = Column(Float, nullable=True, default=None) + funding_fees: Mapped[Optional[float]] = mapped_column( + Float(), nullable=True, default=None) # type: ignore def __init__(self, **kwargs): super().__init__(**kwargs) self.realized_profit = 0 self.recalc_open_trade_value() + @validates('enter_tag', 'exit_reason') + def validate_string_len(self, key, value): + max_len = getattr(self.__class__, key).prop.columns[0].type.length + if value and len(value) > max_len: + return value[:max_len] + return value + def delete(self) -> None: for order in self.orders: - Order.query.session.delete(order) + Order.session.delete(order) - Trade.query.session.delete(self) + Trade.session.delete(self) Trade.commit() @staticmethod def commit(): - Trade.query.session.commit() + Trade.session.commit() @staticmethod def rollback(): - Trade.query.session.rollback() + Trade.session.rollback() @staticmethod - def get_trades_proxy(*, pair: str = None, is_open: bool = None, - open_date: datetime = None, close_date: datetime = None, + def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, + open_date: Optional[datetime] = None, + close_date: Optional[datetime] = None, ) -> List['LocalTrade']: """ Helper function to query Trades.j @@ -1278,7 +1347,7 @@ class Trade(_DECL_BASE, LocalTrade): trade_filter.append(Trade.close_date > close_date) if is_open is not None: trade_filter.append(Trade.is_open.is_(is_open)) - return Trade.get_trades(trade_filter).all() + return cast(List[LocalTrade], Trade.get_trades(trade_filter).all()) else: return LocalTrade.get_trades_proxy( pair=pair, is_open=is_open, @@ -1287,7 +1356,7 @@ class Trade(_DECL_BASE, LocalTrade): ) @staticmethod - def get_trades(trade_filter=None, include_orders: bool = True) -> Query: + def get_trades_query(trade_filter=None, include_orders: bool = True) -> Select: """ Helper function to query Trades using filters. NOTE: Not supported in Backtesting. @@ -1302,22 +1371,35 @@ class Trade(_DECL_BASE, LocalTrade): if trade_filter is not None: if not isinstance(trade_filter, list): trade_filter = [trade_filter] - this_query = Trade.query.filter(*trade_filter) + this_query = select(Trade).filter(*trade_filter) else: - this_query = Trade.query + this_query = select(Trade) if not include_orders: # Don't load order relations # Consider using noload or raiseload instead of lazyload this_query = this_query.options(lazyload(Trade.orders)) return this_query + @staticmethod + def get_trades(trade_filter=None, include_orders: bool = True) -> ScalarResult['Trade']: + """ + Helper function to query Trades using filters. + NOTE: Not supported in Backtesting. + :param trade_filter: Optional filter to apply to trades + Can be either a Filter object, or a List of filters + e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])` + e.g. `(trade_filter=Trade.id == trade_id)` + :return: unsorted query object + """ + return Trade.session.scalars(Trade.get_trades_query(trade_filter, include_orders)) + @staticmethod def get_open_order_trades() -> List['Trade']: """ Returns all open trades NOTE: Not supported in Backtesting. """ - return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + return cast(List[Trade], Trade.get_trades(Trade.open_order_id.isnot(None)).all()) @staticmethod def get_open_trades_without_assigned_fees(): @@ -1347,11 +1429,12 @@ class Trade(_DECL_BASE, LocalTrade): Retrieves total realized profit """ if Trade.use_db: - total_profit = Trade.query.with_entities( - func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)).scalar() + total_profit: float = Trade.session.execute( + select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)) + ).scalar_one() else: - total_profit = sum( - t.close_profit_abs for t in LocalTrade.get_trades_proxy(is_open=False)) + total_profit = sum(t.close_profit_abs # type: ignore + for t in LocalTrade.get_trades_proxy(is_open=False)) return total_profit or 0 @staticmethod @@ -1361,8 +1444,9 @@ class Trade(_DECL_BASE, LocalTrade): in stake currency """ if Trade.use_db: - total_open_stake_amount = Trade.query.with_entities( - func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)).scalar() + total_open_stake_amount = Trade.session.scalar( + select(func.sum(Trade.stake_amount)).filter(Trade.is_open.is_(True)) + ) else: total_open_stake_amount = sum( t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True)) @@ -1374,19 +1458,22 @@ class Trade(_DECL_BASE, LocalTrade): Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ - filters = [Trade.is_open.is_(False)] + filters: List = [Trade.is_open.is_(False)] if minutes: start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) filters.append(Trade.close_date >= start_date) - pair_rates = Trade.query.with_entities( - Trade.pair, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(*filters)\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() + + pair_rates = Trade.session.execute( + select( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(*filters) + .group_by(Trade.pair) + .order_by(desc('profit_sum_abs')) + ).all() + return [ { 'pair': pair, @@ -1407,19 +1494,20 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. """ - filters = [Trade.is_open.is_(False)] + filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) - enter_tag_perf = Trade.query.with_entities( - Trade.enter_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(*filters)\ - .group_by(Trade.enter_tag) \ - .order_by(desc('profit_sum_abs')) \ - .all() + enter_tag_perf = Trade.session.execute( + select( + Trade.enter_tag, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(*filters) + .group_by(Trade.enter_tag) + .order_by(desc('profit_sum_abs')) + ).all() return [ { @@ -1440,19 +1528,19 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. """ - filters = [Trade.is_open.is_(False)] + filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) - - sell_tag_perf = Trade.query.with_entities( - Trade.exit_reason, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(*filters)\ - .group_by(Trade.exit_reason) \ - .order_by(desc('profit_sum_abs')) \ - .all() + sell_tag_perf = Trade.session.execute( + select( + Trade.exit_reason, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(*filters) + .group_by(Trade.exit_reason) + .order_by(desc('profit_sum_abs')) + ).all() return [ { @@ -1473,21 +1561,21 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. """ - filters = [Trade.is_open.is_(False)] + filters: List = [Trade.is_open.is_(False)] if (pair is not None): filters.append(Trade.pair == pair) - - mix_tag_perf = Trade.query.with_entities( - Trade.id, - Trade.enter_tag, - Trade.exit_reason, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(*filters)\ - .group_by(Trade.id) \ - .order_by(desc('profit_sum_abs')) \ - .all() + mix_tag_perf = Trade.session.execute( + select( + Trade.id, + Trade.enter_tag, + Trade.exit_reason, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(*filters) + .group_by(Trade.id) + .order_by(desc('profit_sum_abs')) + ).all() return_list: List[Dict] = [] for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf: @@ -1523,11 +1611,15 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. :returns: Tuple containing (pair, profit_sum) """ - best_pair = Trade.query.with_entities( - Trade.pair, func.sum(Trade.close_profit).label('profit_sum') - ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) \ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum')).first() + best_pair = Trade.session.execute( + select( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum') + ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date)) + .group_by(Trade.pair) + .order_by(desc('profit_sum')) + ).first() + return best_pair @staticmethod @@ -1537,12 +1629,13 @@ class Trade(_DECL_BASE, LocalTrade): NOTE: Not supported in Backtesting. :returns: Tuple containing (pair, profit_sum) """ - trading_volume = Order.query.with_entities( - func.sum(Order.cost).label('volume') - ).filter( - Order.order_filled_date >= start_date, - Order.status == 'closed' - ).scalar() + trading_volume = Trade.session.execute( + select( + func.sum(Order.cost).label('volume') + ).filter( + Order.order_filled_date >= start_date, + Order.status == 'closed' + )).scalar_one() return trading_volume @staticmethod @@ -1591,8 +1684,10 @@ class Trade(_DECL_BASE, LocalTrade): stop_loss=data["stop_loss_abs"], stop_loss_pct=data["stop_loss_ratio"], stoploss_order_id=data["stoploss_order_id"], - stoploss_last_update=(datetime.fromtimestamp(data["stoploss_last_update"] // 1000, - tz=timezone.utc) if data["stoploss_last_update"] else None), + stoploss_last_update=( + datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000, + tz=timezone.utc) + if data["stoploss_last_update_timestamp"] else None), initial_stop_loss=data["initial_stop_loss_abs"], initial_stop_loss_pct=data["initial_stop_loss_ratio"], min_rate=data["min_rate"], diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 9c8787242..7fd20f041 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime, timezone from pathlib import Path from typing import Dict, List, Optional @@ -436,11 +437,11 @@ def create_scatter( return None -def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, - indicators1: List[str] = [], - indicators2: List[str] = [], - plot_config: Dict[str, Dict] = {}, - ) -> go.Figure: +def generate_candlestick_graph( + pair: str, data: pd.DataFrame, trades: Optional[pd.DataFrame] = None, *, + indicators1: List[str] = [], indicators2: List[str] = [], + plot_config: Dict[str, Dict] = {}, + ) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators @@ -632,10 +633,10 @@ def load_and_plot_trades(config: Config): """ strategy = StrategyResolver.load_strategy(config) - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) + exchange = ExchangeResolver.load_exchange(config) IStrategy.dp = DataProvider(config, exchange) strategy.ft_bot_start() - strategy.bot_loop_start() + strategy.bot_loop_start(datetime.now(timezone.utc)) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) timerange = plot_elements['timerange'] trades = plot_elements['trades'] @@ -677,7 +678,7 @@ def plot_profit(config: Config) -> None: if 'timeframe' not in config: raise OperationalException('Timeframe must be set in either config or via --timeframe.') - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) + exchange = ExchangeResolver.load_exchange(config) plot_elements = init_plotscript(config, list(exchange.markets)) trades = plot_elements['trades'] # Filter trades to relevant pairs diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index f9c02e250..2af86592f 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -3,9 +3,9 @@ Minimum age (days listed) pair list filter """ import logging from copy import deepcopy +from datetime import timedelta from typing import Any, Dict, List, Optional -import arrow from pandas import DataFrame from freqtrade.constants import Config, ListPairsWithTimeframes @@ -13,7 +13,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList -from freqtrade.util import PeriodicCache +from freqtrade.util import PeriodicCache, dt_floor_day, dt_now, dt_ts logger = logging.getLogger(__name__) @@ -84,10 +84,7 @@ class AgeFilter(IPairList): since_days = -( self._max_days_listed if self._max_days_listed else self._min_days_listed ) - 1 - since_ms = int(arrow.utcnow() - .floor('day') - .shift(days=since_days) - .float_timestamp) * 1000 + since_ms = dt_ts(dt_floor_day(dt_now()) + timedelta(days=since_days)) candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False) if self._enabled: for p in deepcopy(pairlist): @@ -116,7 +113,7 @@ class AgeFilter(IPairList): ): # We have fetched at least the minimum required number of daily candles # Add to cache, store the time we last checked this symbol - self._symbolsChecked[pair] = arrow.utcnow().int_timestamp * 1000 + self._symbolsChecked[pair] = dt_ts() return True else: self.log_once(( @@ -127,6 +124,6 @@ class AgeFilter(IPairList): " or more than " f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" ) if self._max_days_listed else ''), logger.info) - self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000 + self._symbolsCheckFailed[pair] = dt_ts() return False return False diff --git a/freqtrade/plugins/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py index 478eaec20..2e74aa293 100644 --- a/freqtrade/plugins/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -6,6 +6,7 @@ from typing import Any, Dict, Optional from freqtrade.constants import Config from freqtrade.exceptions import OperationalException +from freqtrade.exchange import ROUND_UP from freqtrade.exchange.types import Ticker from freqtrade.plugins.pairlist.IPairList import IPairList @@ -61,9 +62,10 @@ class PrecisionFilter(IPairList): stop_price = ticker['last'] * self._stoploss # Adjust stop-prices to precision - sp = self._exchange.price_to_precision(pair, stop_price) + sp = self._exchange.price_to_precision(pair, stop_price, rounding_mode=ROUND_UP) - stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99) + stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99, + rounding_mode=ROUND_UP) logger.debug(f"{pair} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index b54be1fa7..d077330e0 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -143,6 +143,9 @@ class RemotePairList(IPairList): if self._init_done: pairlist = self._pair_cache.get('pairlist') + if pairlist == [None]: + # Valid but empty pairlist. + return [] else: pairlist = [] @@ -157,7 +160,7 @@ class RemotePairList(IPairList): file_path = Path(filename) if file_path.exists(): - with open(filename) as json_file: + with file_path.open() as json_file: # Load the JSON data into a dictionary jsonparse = json.load(json_file) @@ -181,7 +184,11 @@ class RemotePairList(IPairList): pairlist = self._whitelist_for_active_markets(pairlist) pairlist = pairlist[:self._number_pairs] - self._pair_cache['pairlist'] = pairlist.copy() + if pairlist: + self._pair_cache['pairlist'] = pairlist.copy() + else: + # If pairlist is empty, set a dummy value to avoid fetching again + self._pair_cache['pairlist'] = [None] if time_elapsed != 0.0: self.log_once(f'Pairlist Fetched in {time_elapsed} seconds.', logger.info) diff --git a/freqtrade/plugins/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py index 207328d08..d47b68568 100644 --- a/freqtrade/plugins/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -5,6 +5,7 @@ import logging from typing import Any, Dict, Optional from freqtrade.constants import Config +from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Ticker from freqtrade.plugins.pairlist.IPairList import IPairList @@ -22,6 +23,12 @@ class SpreadFilter(IPairList): self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005) self._enabled = self._max_spread_ratio != 0 + if not self._exchange.get_option('tickers_have_bid_ask'): + raise OperationalException( + f"{self.name} requires exchange to have bid/ask data for tickers, " + "which is not available for the selected exchange / trading mode." + ) + @property def needstickers(self) -> bool: """ diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 401a2e86c..9196026bb 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -4,9 +4,9 @@ Volatility pairlist filter import logging import sys from copy import deepcopy +from datetime import timedelta from typing import Any, Dict, List, Optional -import arrow import numpy as np from cachetools import TTLCache from pandas import DataFrame @@ -16,6 +16,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.util import dt_floor_day, dt_now, dt_ts logger = logging.getLogger(__name__) @@ -73,10 +74,7 @@ class VolatilityFilter(IPairList): needed_pairs: ListPairsWithTimeframes = [ (p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache] - since_ms = (arrow.utcnow() - .floor('day') - .shift(days=-self._days - 1) - .int_timestamp) * 1000 + since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days - 1)) # Get all candles candles = {} if needed_pairs: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 2649a8425..b9c312f87 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -4,7 +4,7 @@ Volume PairList provider Provides dynamic pair list based on trade volumes """ import logging -from datetime import datetime, timedelta, timezone +from datetime import timedelta from typing import Any, Dict, List, Literal from cachetools import TTLCache @@ -15,6 +15,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange.types import Tickers from freqtrade.misc import format_ms_time from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.util import dt_now logger = logging.getLogger(__name__) @@ -161,13 +162,13 @@ class VolumePairList(IPairList): # get lookback period in ms, for exchange ohlcv fetch since_ms = int(timeframe_to_prev_date( self._lookback_timeframe, - datetime.now(timezone.utc) + timedelta( + dt_now() + timedelta( minutes=-(self._lookback_period * self._tf_in_min) - self._tf_in_min) ).timestamp()) * 1000 to_ms = int(timeframe_to_prev_date( self._lookback_timeframe, - datetime.now(timezone.utc) - timedelta(minutes=self._tf_in_min) + dt_now() - timedelta(minutes=self._tf_in_min) ).timestamp()) * 1000 # todo: utc date output for starting date diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 546b026cb..1181b2812 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -3,9 +3,9 @@ Rate of change pairlist filter """ import logging from copy import deepcopy +from datetime import timedelta from typing import Any, Dict, List, Optional -import arrow from cachetools import TTLCache from pandas import DataFrame @@ -14,6 +14,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.util import dt_floor_day, dt_now, dt_ts logger = logging.getLogger(__name__) @@ -71,10 +72,7 @@ class RangeStabilityFilter(IPairList): needed_pairs: ListPairsWithTimeframes = [ (p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache] - since_ms = (arrow.utcnow() - .floor('day') - .shift(days=-self._days - 1) - .int_timestamp) * 1000 + since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days - 1)) # Get all candles candles = {} if needed_pairs: diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 20a264fd8..b300f06be 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -23,7 +23,8 @@ logger = logging.getLogger(__name__) class PairListManager(LoggingMixin): - def __init__(self, exchange, config: Config, dataprovider: DataProvider = None) -> None: + def __init__( + self, exchange, config: Config, dataprovider: Optional[DataProvider] = None) -> None: self._exchange = exchange self._config = config self._whitelist = self._config['exchange'].get('pair_whitelist') @@ -153,7 +154,8 @@ class PairListManager(LoggingMixin): return [] return whitelist - def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes: + def create_pair_list( + self, pairs: List[str], timeframe: Optional[str] = None) -> ListPairsWithTimeframes: """ Create list of pair tuples with (pair, timeframe) """ diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 54a488e8d..c5c4e1a68 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -2,9 +2,10 @@ This module loads custom exchanges """ import logging +from typing import Optional import freqtrade.exchange as exchanges -from freqtrade.constants import Config +from freqtrade.constants import Config, ExchangeConfig from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, Exchange from freqtrade.resolvers import IResolver @@ -19,13 +20,14 @@ class ExchangeResolver(IResolver): object_type = Exchange @staticmethod - def load_exchange(exchange_name: str, config: Config, validate: bool = True, - load_leverage_tiers: bool = False) -> Exchange: + def load_exchange(config: Config, *, exchange_config: Optional[ExchangeConfig] = None, + validate: bool = True, load_leverage_tiers: bool = False) -> Exchange: """ Load the custom class from config parameter :param exchange_name: name of the Exchange to load :param config: configuration dictionary """ + exchange_name: str = config['exchange']['name'] # Map exchange name to avoid duplicate classes for identical exchanges exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name) exchange_name = exchange_name.title() @@ -36,13 +38,14 @@ class ExchangeResolver(IResolver): kwargs={ 'config': config, 'validate': validate, + 'exchange_config': exchange_config, 'load_leverage_tiers': load_leverage_tiers} ) except ImportError: logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") if not exchange: - exchange = Exchange(config, validate=validate) + exchange = Exchange(config, validate=validate, exchange_config=exchange_config,) return exchange @staticmethod diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 0b484394a..2b20560e2 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -89,7 +89,8 @@ class IResolver: module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: + except (AttributeError, ModuleNotFoundError, SyntaxError, + ImportError, NameError) as err: # Catch errors in case a specific module is not installed logger.warning(f"Could not import {module_path} due to '{err}'") if enum_failed: diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 67df49dcb..6f5b6655d 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -33,7 +33,7 @@ class StrategyResolver(IResolver): extra_path = "strategy_path" @staticmethod - def load_strategy(config: Config = None) -> IStrategy: + def load_strategy(config: Optional[Config] = None) -> IStrategy: """ Load the custom class from config parameter :param config: configuration dictionary or None @@ -76,6 +76,7 @@ class StrategyResolver(IResolver): ("ignore_buying_expired_candle_after", 0), ("position_adjustment_enable", False), ("max_entry_position_adjustment", -1), + ("max_open_trades", -1) ] for attribute, default in attributes: StrategyResolver._override_attribute_helper(strategy, config, @@ -110,7 +111,11 @@ class StrategyResolver(IResolver): val = getattr(strategy, attribute) # None's cannot exist in the config, so do not copy them if val is not None: - config[attribute] = val + # max_open_trades set to -1 in the strategy will be copied as infinity in the config + if attribute == 'max_open_trades' and val == -1: + config[attribute] = float('inf') + else: + config[attribute] = val # Explicitly check for None here as other "falsy" values are possible elif default is not None: setattr(strategy, attribute, default) @@ -128,6 +133,8 @@ class StrategyResolver(IResolver): key=lambda t: t[0])) if hasattr(strategy, 'stoploss'): strategy.stoploss = float(strategy.stoploss) + if hasattr(strategy, 'max_open_trades') and strategy.max_open_trades < 0: + strategy.max_open_trades = float('inf') return strategy @staticmethod diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index 957565e2c..07f83abc0 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -1,3 +1,2 @@ -# flake8: noqa: F401 -from .rpc import RPC, RPCException, RPCHandler -from .rpc_manager import RPCManager +from .rpc import RPC, RPCException, RPCHandler # noqa: F401 +from .rpc_manager import RPCManager # noqa: F401 diff --git a/freqtrade/rpc/api_server/__init__.py b/freqtrade/rpc/api_server/__init__.py index df255c186..b2ed3e6e0 100644 --- a/freqtrade/rpc/api_server/__init__.py +++ b/freqtrade/rpc/api_server/__init__.py @@ -1,2 +1 @@ -# flake8: noqa: F401 -from .webserver import ApiServer +from .webserver import ApiServer # noqa: F401 diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index bc2a40d91..8fa1a87b8 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -8,14 +8,16 @@ from fastapi import APIRouter, BackgroundTasks, Depends from fastapi.exceptions import HTTPException from freqtrade.configuration.config_validation import validate_config_consistency +from freqtrade.constants import Config from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result from freqtrade.enums import BacktestState -from freqtrade.exceptions import DependencyException +from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exchange.common import remove_exchange_credentials from freqtrade.misc import deep_merge_dicts from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, BacktestResponse) from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode -from freqtrade.rpc.api_server.webserver import ApiServer +from freqtrade.rpc.api_server.webserver_bgwork import ApiBG from freqtrade.rpc.rpc import RPCException @@ -25,18 +27,92 @@ logger = logging.getLogger(__name__) router = APIRouter() +def __run_backtest_bg(btconfig: Config): + from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_stats + from freqtrade.resolvers import StrategyResolver + asyncio.set_event_loop(asyncio.new_event_loop()) + try: + # Reload strategy + lastconfig = ApiBG.bt['last_config'] + strat = StrategyResolver.load_strategy(btconfig) + validate_config_consistency(btconfig) + + if ( + not ApiBG.bt['bt'] + or lastconfig.get('timeframe') != strat.timeframe + or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') + or lastconfig.get('timerange') != btconfig['timerange'] + ): + from freqtrade.optimize.backtesting import Backtesting + ApiBG.bt['bt'] = Backtesting(btconfig) + ApiBG.bt['bt'].load_bt_data_detail() + else: + ApiBG.bt['bt'].config = btconfig + ApiBG.bt['bt'].init_backtest() + # Only reload data if timeframe changed. + if ( + not ApiBG.bt['data'] + or not ApiBG.bt['timerange'] + or lastconfig.get('timeframe') != strat.timeframe + or lastconfig.get('timerange') != btconfig['timerange'] + ): + ApiBG.bt['data'], ApiBG.bt['timerange'] = ApiBG.bt[ + 'bt'].load_bt_data() + + lastconfig['timerange'] = btconfig['timerange'] + lastconfig['timeframe'] = strat.timeframe + lastconfig['protections'] = btconfig.get('protections', []) + lastconfig['enable_protections'] = btconfig.get('enable_protections') + lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet') + + ApiBG.bt['bt'].enable_protections = btconfig.get('enable_protections', False) + ApiBG.bt['bt'].strategylist = [strat] + ApiBG.bt['bt'].results = {} + ApiBG.bt['bt'].load_prior_backtest() + + ApiBG.bt['bt'].abort = False + if (ApiBG.bt['bt'].results and + strat.get_strategy_name() in ApiBG.bt['bt'].results['strategy']): + # When previous result hash matches - reuse that result and skip backtesting. + logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') + else: + min_date, max_date = ApiBG.bt['bt'].backtest_one_strategy( + strat, ApiBG.bt['data'], ApiBG.bt['timerange']) + + ApiBG.bt['bt'].results = generate_backtest_stats( + ApiBG.bt['data'], ApiBG.bt['bt'].all_results, + min_date=min_date, max_date=max_date) + + if btconfig.get('export', 'none') == 'trades': + store_backtest_stats( + btconfig['exportfilename'], ApiBG.bt['bt'].results, + datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ) + + logger.info("Backtest finished.") + + except (Exception, OperationalException, DependencyException) as e: + logger.exception(f"Backtesting caused an error: {e}") + ApiBG.bt['bt_error'] = str(e) + pass + finally: + ApiBG.bgtask_running = False + + @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) -# flake8: noqa: C901 -async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, - config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): +async def api_start_backtest( + bt_settings: BacktestRequest, background_tasks: BackgroundTasks, + config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): + ApiBG.bt['bt_error'] = None """Start backtesting if not done so already""" - if ApiServer._bgtask_running: + if ApiBG.bgtask_running: raise RPCException('Bot Background task already running') if ':' in bt_settings.strategy: raise HTTPException(status_code=500, detail="base64 encoded strategies are not allowed.") btconfig = deepcopy(config) + remove_exchange_credentials(btconfig['exchange'], True) settings = dict(bt_settings) if settings.get('freqai', None) is not None: settings['freqai'] = dict(settings['freqai']) @@ -53,78 +129,9 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac # Start backtesting # Initialize backtesting object - def run_backtest(): - from freqtrade.optimize.optimize_reports import (generate_backtest_stats, - store_backtest_stats) - from freqtrade.resolvers import StrategyResolver - asyncio.set_event_loop(asyncio.new_event_loop()) - try: - # Reload strategy - lastconfig = ApiServer._bt_last_config - strat = StrategyResolver.load_strategy(btconfig) - validate_config_consistency(btconfig) - if ( - not ApiServer._bt - or lastconfig.get('timeframe') != strat.timeframe - or lastconfig.get('timeframe_detail') != btconfig.get('timeframe_detail') - or lastconfig.get('timerange') != btconfig['timerange'] - ): - from freqtrade.optimize.backtesting import Backtesting - ApiServer._bt = Backtesting(btconfig) - ApiServer._bt.load_bt_data_detail() - else: - ApiServer._bt.config = btconfig - ApiServer._bt.init_backtest() - # Only reload data if timeframe changed. - if ( - not ApiServer._bt_data - or not ApiServer._bt_timerange - or lastconfig.get('timeframe') != strat.timeframe - or lastconfig.get('timerange') != btconfig['timerange'] - ): - ApiServer._bt_data, ApiServer._bt_timerange = ApiServer._bt.load_bt_data() - - lastconfig['timerange'] = btconfig['timerange'] - lastconfig['timeframe'] = strat.timeframe - lastconfig['protections'] = btconfig.get('protections', []) - lastconfig['enable_protections'] = btconfig.get('enable_protections') - lastconfig['dry_run_wallet'] = btconfig.get('dry_run_wallet') - - ApiServer._bt.enable_protections = btconfig.get('enable_protections', False) - ApiServer._bt.strategylist = [strat] - ApiServer._bt.results = {} - ApiServer._bt.load_prior_backtest() - - ApiServer._bt.abort = False - if (ApiServer._bt.results and - strat.get_strategy_name() in ApiServer._bt.results['strategy']): - # When previous result hash matches - reuse that result and skip backtesting. - logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}') - else: - min_date, max_date = ApiServer._bt.backtest_one_strategy( - strat, ApiServer._bt_data, ApiServer._bt_timerange) - - ApiServer._bt.results = generate_backtest_stats( - ApiServer._bt_data, ApiServer._bt.all_results, - min_date=min_date, max_date=max_date) - - if btconfig.get('export', 'none') == 'trades': - store_backtest_stats( - btconfig['exportfilename'], ApiServer._bt.results, - datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - ) - - logger.info("Backtest finished.") - - except DependencyException as e: - logger.info(f"Backtesting caused an error: {e}") - pass - finally: - ApiServer._bgtask_running = False - - background_tasks.add_task(run_backtest) - ApiServer._bgtask_running = True + background_tasks.add_task(__run_backtest_bg, btconfig=btconfig) + ApiBG.bgtask_running = True return { "status": "running", @@ -142,17 +149,18 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): Returns Result after backtesting has been ran. """ from freqtrade.persistence import LocalTrade - if ApiServer._bgtask_running: + if ApiBG.bgtask_running: return { "status": "running", "running": True, - "step": ApiServer._bt.progress.action if ApiServer._bt else str(BacktestState.STARTUP), - "progress": ApiServer._bt.progress.progress if ApiServer._bt else 0, + "step": (ApiBG.bt['bt'].progress.action if ApiBG.bt['bt'] + else str(BacktestState.STARTUP)), + "progress": ApiBG.bt['bt'].progress.progress if ApiBG.bt['bt'] else 0, "trade_count": len(LocalTrade.trades), "status_msg": "Backtest running", } - if not ApiServer._bt: + if not ApiBG.bt['bt']: return { "status": "not_started", "running": False, @@ -160,6 +168,14 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest not yet executed" } + if ApiBG.bt['bt_error']: + return { + "status": "error", + "running": False, + "step": "", + "progress": 0, + "status_msg": f"Backtest failed with {ApiBG.bt['bt_error']}" + } return { "status": "ended", @@ -167,14 +183,14 @@ def api_get_backtest(ws_mode=Depends(is_webserver_mode)): "status_msg": "Backtest ended", "step": "finished", "progress": 1, - "backtest_result": ApiServer._bt.results, + "backtest_result": ApiBG.bt['bt'].results, } @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): """Reset backtesting""" - if ApiServer._bgtask_running: + if ApiBG.bgtask_running: return { "status": "running", "running": True, @@ -182,12 +198,12 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest running", } - if ApiServer._bt: - ApiServer._bt.cleanup() - del ApiServer._bt - ApiServer._bt = None - del ApiServer._bt_data - ApiServer._bt_data = None + if ApiBG.bt['bt']: + ApiBG.bt['bt'].cleanup() + del ApiBG.bt['bt'] + ApiBG.bt['bt'] = None + del ApiBG.bt['data'] + ApiBG.bt['data'] = None logger.info("Backtesting reset") return { "status": "reset", @@ -200,7 +216,7 @@ def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): - if not ApiServer._bgtask_running: + if not ApiBG.bgtask_running: return { "status": "not_running", "running": False, @@ -208,7 +224,7 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): "progress": 0, "status_msg": "Backtest ended", } - ApiServer._bt.abort = True + ApiBG.bt['bt'].abort = True return { "status": "stopping", "running": False, @@ -218,14 +234,17 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): } -@router.get('/backtest/history', response_model=List[BacktestHistoryEntry], tags=['webserver', 'backtest']) +@router.get('/backtest/history', response_model=List[BacktestHistoryEntry], + tags=['webserver', 'backtest']) def api_backtest_history(config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): # Get backtest result history, read from metadata files return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results') -@router.get('/backtest/history/result', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): +@router.get('/backtest/history/result', response_model=BacktestResponse, + tags=['webserver', 'backtest']) +def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config), + ws_mode=Depends(is_webserver_mode)): # Get backtest result history, read from metadata files fn = config['user_data_dir'] / 'backtest_results' / filename results: Dict[str, Any] = { diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 404d64d16..a081f9fe9 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode @@ -36,20 +36,25 @@ class Balance(BaseModel): free: float balance: float used: float + bot_owned: Optional[float] est_stake: float + est_stake_bot: Optional[float] stake: str # Starting with 2.x side: str leverage: float is_position: bool position: float + is_bot_managed: bool class Balances(BaseModel): currencies: List[Balance] total: float + total_bot: float symbol: str value: float + value_bot: float stake: str note: str starting_capital: float @@ -95,8 +100,10 @@ class Profit(BaseModel): trade_count: int closed_trade_count: int first_trade_date: str + first_trade_humanized: str first_trade_timestamp: int latest_trade_date: str + latest_trade_humanized: str latest_trade_timestamp: int avg_duration: str best_pair: str @@ -108,6 +115,8 @@ class Profit(BaseModel): max_drawdown: float max_drawdown_abs: float trading_volume: Optional[float] + bot_start_timestamp: int + bot_start_date: str class SellReason(BaseModel): @@ -165,9 +174,10 @@ class ShowConfig(BaseModel): stake_amount: str available_capital: Optional[float] stake_currency_decimals: int - max_open_trades: int + max_open_trades: IntOrInf minimal_roi: Dict[str, Any] stoploss: Optional[float] + stoploss_on_exchange: bool trailing_stop: Optional[bool] trailing_stop_positive: Optional[float] trailing_stop_positive_offset: Optional[float] @@ -227,24 +237,33 @@ class TradeSchema(BaseModel): fee_close: Optional[float] fee_close_cost: Optional[float] fee_close_currency: Optional[str] + open_date: str open_timestamp: int open_rate: float open_rate_requested: Optional[float] open_trade_value: float + close_date: Optional[str] close_timestamp: Optional[int] close_rate: Optional[float] close_rate_requested: Optional[float] + close_profit: Optional[float] close_profit_pct: Optional[float] close_profit_abs: Optional[float] + profit_ratio: Optional[float] profit_pct: Optional[float] profit_abs: Optional[float] profit_fiat: Optional[float] + + realized_profit: float + realized_profit_ratio: Optional[float] + exit_reason: Optional[str] exit_order_status: Optional[str] + stop_loss_abs: Optional[float] stop_loss_ratio: Optional[float] stop_loss_pct: Optional[float] @@ -254,6 +273,7 @@ class TradeSchema(BaseModel): initial_stop_loss_abs: Optional[float] initial_stop_loss_ratio: Optional[float] initial_stop_loss_pct: Optional[float] + min_rate: Optional[float] max_rate: Optional[float] open_order_id: Optional[str] @@ -265,6 +285,10 @@ class TradeSchema(BaseModel): funding_fees: Optional[float] trading_mode: Optional[TradingMode] + amount_precision: Optional[float] + price_precision: Optional[float] + precision_mode: Optional[int] + class OpenTradeSchema(TradeSchema): stoploss_current_dist: Optional[float] @@ -272,10 +296,11 @@ class OpenTradeSchema(TradeSchema): stoploss_current_dist_ratio: Optional[float] stoploss_entry_dist: Optional[float] stoploss_entry_dist_ratio: Optional[float] - current_profit: float - current_profit_abs: float - current_profit_pct: float current_rate: float + total_profit_abs: float + total_profit_fiat: Optional[float] + total_profit_ratio: Optional[float] + open_order: Optional[str] @@ -299,7 +324,7 @@ class LockModel(BaseModel): lock_timestamp: int pair: str side: str - reason: str + reason: Optional[str] class Locks(BaseModel): @@ -422,7 +447,7 @@ class BacktestRequest(BaseModel): timeframe: Optional[str] timeframe_detail: Optional[str] timerange: Optional[str] - max_open_trades: Optional[int] + max_open_trades: Optional[IntOrInf] stake_amount: Optional[str] enable_protections: bool dry_run_wallet: Optional[float] @@ -455,5 +480,5 @@ class SysInfo(BaseModel): class Health(BaseModel): - last_process: datetime - last_process_ts: int + last_process: Optional[datetime] + last_process_ts: Optional[int] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index e26df6eea..2354c4bf8 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -40,7 +40,13 @@ logger = logging.getLogger(__name__) # 2.20: Add websocket endpoints # 2.21: Add new_candle messagetype # 2.22: Add FreqAI to backtesting -API_VERSION = 2.22 +# 2.23: Allow plot config request in webserver mode +# 2.24: Add cancel_open_order endpoint +# 2.25: Add several profit values to /status endpoint +# 2.26: increase /balance output +# 2.27: Add /trades//reload endpoint +# 2.28: Switch reload endpoint to Post +API_VERSION = 2.28 # Public API, requires no auth. router_public = APIRouter() @@ -122,6 +128,18 @@ def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete(tradeid) +@router.delete('/trades/{tradeid}/open-order', response_model=OpenTradeSchema, tags=['trading']) +def trade_cancel_open_order(tradeid: int, rpc: RPC = Depends(get_rpc)): + rpc._rpc_cancel_open_order(tradeid) + return rpc._rpc_trade_status([tradeid])[0] + + +@router.post('/trades/{tradeid}/reload', response_model=OpenTradeSchema, tags=['trading']) +def trade_reload(tradeid: int, rpc: RPC = Depends(get_rpc)): + rpc._rpc_reload_trade_from_exchange(tradeid) + return rpc._rpc_trade_status([tradeid])[0] + + # TODO: Missing response model @router.get('/edge', tags=['info']) def edge(rpc: RPC = Depends(get_rpc)): @@ -237,19 +255,32 @@ def pair_candles( @router.get('/pair_history', response_model=PairHistory, tags=['candle data']) def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, + freqaimodel: Optional[str] = None, config=Depends(get_config), exchange=Depends(get_exchange)): # The initial call to this endpoint can be slow, as it may need to initialize # the exchange class. config = deepcopy(config) config.update({ 'strategy': strategy, + 'timerange': timerange, + 'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'), }) - return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange, exchange) + return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange) @router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) -def plot_config(rpc: RPC = Depends(get_rpc)): - return PlotConfig.parse_obj(rpc._rpc_plot_config()) +def plot_config(strategy: Optional[str] = None, config=Depends(get_config), + rpc: Optional[RPC] = Depends(get_rpc_optional)): + if not strategy: + if not rpc: + raise RPCException("Strategy is mandatory in webserver mode.") + return PlotConfig.parse_obj(rpc._rpc_plot_config()) + else: + config1 = deepcopy(config) + config1.update({ + 'strategy': strategy + }) + return PlotConfig.parse_obj(RPC._rpc_plot_config_with_strategy(config1)) @router.get('/strategies', response_model=StrategyListResponse, tags=['strategy']) @@ -284,11 +315,11 @@ def get_strategy(strategy: str, config=Depends(get_config)): @router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai']) def list_freqaimodels(config=Depends(get_config)): from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver - strategies = FreqaiModelResolver.search_all_objects( + models = FreqaiModelResolver.search_all_objects( config, False) - strategies = sorted(strategies, key=lambda x: x['name']) + models = sorted(models, key=lambda x: x['name']) - return {'freqaimodels': [x['name'] for x in strategies]} + return {'freqaimodels': [x['name'] for x in models]} @router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data']) @@ -328,4 +359,4 @@ def sysinfo(): @router.get('/health', response_model=Health, tags=['info']) def health(rpc: RPC = Depends(get_rpc)): - return rpc._health() + return rpc.health() diff --git a/freqtrade/rpc/api_server/api_ws.py b/freqtrade/rpc/api_server/api_ws.py index 18714f15f..b253d66c2 100644 --- a/freqtrade/rpc/api_server/api_ws.py +++ b/freqtrade/rpc/api_server/api_ws.py @@ -90,7 +90,7 @@ async def _process_consumer_request( elif type == RPCRequestType.ANALYZED_DF: # Limit the amount of candles per dataframe to 'limit' or 1500 - limit = min(data.get('limit', 1500), 1500) if data else None + limit = int(min(data.get('limit', 1500), 1500)) if data else None pair = data.get('pair', None) if data else None # For every pair in the generator, send a separate message diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index aed97367b..8fd105d3e 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -1,9 +1,12 @@ -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.api_server.webserver_bgwork import ApiBG from freqtrade.rpc.rpc import RPC, RPCException from .webserver import ApiServer @@ -15,12 +18,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') @@ -34,11 +44,11 @@ def get_api_config() -> Dict[str, Any]: def get_exchange(config=Depends(get_config)): - if not ApiServer._exchange: + if not ApiBG.exchange: from freqtrade.resolvers import ExchangeResolver - ApiServer._exchange = ExchangeResolver.load_exchange( - config['exchange']['name'], config, load_leverage_tiers=False) - return ApiServer._exchange + ApiBG.exchange = ExchangeResolver.load_exchange( + config, load_leverage_tiers=False) + return ApiBG.exchange def get_message_stream(): diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index a79c1a5fc..48786bec2 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -55,7 +55,7 @@ class UvicornServer(uvicorn.Server): @contextlib.contextmanager def run_in_thread(self): - self.thread = threading.Thread(target=self.run) + self.thread = threading.Thread(target=self.run, name='FTUvicorn') self.thread.start() while not self.started: time.sleep(1e-3) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 92bded1c5..165849a7f 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -1,6 +1,6 @@ import logging from ipaddress import IPv4Address -from typing import Any, Dict, Optional +from typing import Any, Optional import orjson import uvicorn @@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.ws.message_stream import MessageStream from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -35,16 +36,8 @@ class ApiServer(RPCHandler): __initialized = False _rpc: RPC - # Backtesting type: Backtesting - _bt = None - _bt_data = None - _bt_timerange = None - _bt_last_config: Config = {} _has_rpc: bool = False - _bgtask_running: bool = False _config: Config = {} - # Exchange - only available in webserver mode. - _exchange = None # websocket message stuff _message_stream: Optional[MessageStream] = None @@ -81,7 +74,7 @@ class ApiServer(RPCHandler): """ Attach rpc handler """ - if not self._has_rpc: + if not ApiServer._has_rpc: ApiServer._rpc = rpc ApiServer._has_rpc = True else: @@ -105,7 +98,7 @@ class ApiServer(RPCHandler): cls._has_rpc = False cls._rpc = None - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Publish the message to the message stream """ diff --git a/freqtrade/rpc/api_server/webserver_bgwork.py b/freqtrade/rpc/api_server/webserver_bgwork.py new file mode 100644 index 000000000..925f34de3 --- /dev/null +++ b/freqtrade/rpc/api_server/webserver_bgwork.py @@ -0,0 +1,16 @@ + +from typing import Any, Dict + + +class ApiBG(): + # Backtesting type: Backtesting + bt: Dict[str, Any] = { + 'bt': None, + 'data': None, + 'timerange': None, + 'last_config': {}, + 'bt_error': None, + } + bgtask_running: bool = False + # Exchange - only available in webserver mode. + exchange = None diff --git a/freqtrade/rpc/api_server/ws/__init__.py b/freqtrade/rpc/api_server/ws/__init__.py index 0b94d3fee..b76428119 100644 --- a/freqtrade/rpc/api_server/ws/__init__.py +++ b/freqtrade/rpc/api_server/ws/__init__.py @@ -1,7 +1,6 @@ -# flake8: noqa: F401 # isort: off -from freqtrade.rpc.api_server.ws.types import WebSocketType -from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy -from freqtrade.rpc.api_server.ws.serializer import HybridJSONWebSocketSerializer -from freqtrade.rpc.api_server.ws.channel import WebSocketChannel -from freqtrade.rpc.api_server.ws.message_stream import MessageStream +from freqtrade.rpc.api_server.ws.types import WebSocketType # noqa: F401 +from freqtrade.rpc.api_server.ws.proxy import WebSocketProxy # noqa: F401 +from freqtrade.rpc.api_server.ws.serializer import HybridJSONWebSocketSerializer # noqa: F401 +from freqtrade.rpc.api_server.ws.channel import WebSocketChannel # noqa: F401 +from freqtrade.rpc.api_server.ws.message_stream import MessageStream # noqa: F401 diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ed905d844..dedb35503 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -5,30 +5,33 @@ import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone from math import isnan -from typing import Any, Dict, Generator, List, Optional, Tuple, Union +from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, Union -import arrow import psutil from dateutil.relativedelta import relativedelta from dateutil.tz import tzlocal from numpy import NAN, inf, int64, mean from pandas import DataFrame, NaT +from sqlalchemy import func, select from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config from freqtrade.data.history import load_data from freqtrade.data.metrics import calculate_max_drawdown -from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State, - TradingMode) +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection, + State, TradingMode) from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs +from freqtrade.exchange.types import Tickers from freqtrade.loggers import bufferHandler -from freqtrade.misc import decimals_per_coin, shorten_date -from freqtrade.persistence import Order, PairLocks, Trade +from freqtrade.misc import decimals_per_coin +from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter +from freqtrade.rpc.rpc_types import RPCSendMsg +from freqtrade.util import dt_humanize, dt_now, shorten_date from freqtrade.wallets import PositionWallet, Wallet @@ -78,7 +81,7 @@ class RPCHandler: """ Cleanup pending module resources """ @abstractmethod - def send_msg(self, msg: Dict[str, str]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Sends a message to all registered rpc modules """ @@ -122,6 +125,8 @@ class RPC: if config['max_open_trades'] != float('inf') else -1), 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, 'stoploss': config.get('stoploss'), + 'stoploss_on_exchange': config.get('order_types', + {}).get('stoploss_on_exchange', False), 'trailing_stop': config.get('trailing_stop'), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), @@ -157,7 +162,7 @@ class RPC: """ # Fetch open trades if trade_ids: - trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() + trades: Sequence[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all() else: trades = Trade.get_open_trades() @@ -168,6 +173,7 @@ class RPC: for trade in trades: order: Optional[Order] = None current_profit_fiat: Optional[float] = None + total_profit_fiat: Optional[float] = None if trade.open_order_id: order = trade.select_order_by_order_id(trade.open_order_id) # calculate profit and send message to user @@ -187,8 +193,14 @@ class RPC: else: # Closed trade ... current_rate = trade.close_rate - current_profit = trade.close_profit - current_profit_abs = trade.close_profit_abs + current_profit = trade.close_profit or 0.0 + current_profit_abs = trade.close_profit_abs or 0.0 + total_profit_abs = trade.realized_profit + current_profit_abs + total_profit_ratio: Optional[float] = None + if trade.max_stake_amount: + total_profit_ratio = ( + (total_profit_abs / trade.max_stake_amount) * trade.leverage + ) # Calculate fiat profit if not isnan(current_profit_abs) and self._fiat_converter: @@ -197,6 +209,11 @@ class RPC: self._freqtrade.config['stake_currency'], self._freqtrade.config['fiat_display_currency'] ) + total_profit_fiat = self._fiat_converter.convert_amount( + total_profit_abs, + self._freqtrade.config['stake_currency'], + self._freqtrade.config['fiat_display_currency'] + ) # Calculate guaranteed profit (in case of trailing stop) stoploss_entry_dist = trade.calc_profit(trade.stop_loss) @@ -209,14 +226,14 @@ class RPC: trade_dict.update(dict( close_profit=trade.close_profit if not trade.is_open else None, current_rate=current_rate, - current_profit=current_profit, # Deprecated - current_profit_pct=round(current_profit * 100, 2), # Deprecated - current_profit_abs=current_profit_abs, # Deprecated profit_ratio=current_profit, profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, profit_fiat=current_profit_fiat, + total_profit_abs=total_profit_abs, + total_profit_fiat=total_profit_fiat, + total_profit_ratio=total_profit_ratio, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), @@ -275,7 +292,7 @@ class RPC: and open_order.ft_order_side == trade.entry_side) else '') + ('**' if (open_order and open_order.ft_order_side == trade.exit_side is not None) else ''), - shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), + shorten_date(dt_humanize(trade.open_date, only_distance=True)), profit_str ] if self._config.get('position_adjustment_enable', False): @@ -326,11 +343,13 @@ class RPC: for day in range(0, timescale): profitday = start_date - time_offset(day) # Only query for necessary columns for performance reasons. - trades = Trade.query.session.query(Trade.close_profit_abs).filter( - Trade.is_open.is_(False), - Trade.close_date >= profitday, - Trade.close_date < (profitday + time_offset(1)) - ).order_by(Trade.close_date).all() + trades = Trade.session.execute( + select(Trade.close_profit_abs) + .filter(Trade.is_open.is_(False), + Trade.close_date >= profitday, + Trade.close_date < (profitday + time_offset(1))) + .order_by(Trade.close_date) + ).all() curdayprofit = sum( trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) @@ -366,21 +385,27 @@ class RPC: def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ - order_by = Trade.id if order_by_id else Trade.close_date.desc() + order_by: Any = Trade.id if order_by_id else Trade.close_date.desc() if limit: - trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - order_by).limit(limit).offset(offset) + trades = Trade.session.scalars( + Trade.get_trades_query([Trade.is_open.is_(False)]) + .order_by(order_by) + .limit(limit) + .offset(offset)) else: - trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - Trade.close_date.desc()).all() + trades = Trade.session.scalars( + Trade.get_trades_query([Trade.is_open.is_(False)]) + .order_by(Trade.close_date.desc())) output = [trade.to_json() for trade in trades] + total_trades = Trade.session.scalar( + select(func.count(Trade.id)).filter(Trade.is_open.is_(False))) return { "trades": output, "trades_count": len(output), "offset": offset, - "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), + "total_trades": total_trades, } def _rpc_stats(self) -> Dict[str, Any]: @@ -394,17 +419,16 @@ class RPC: return 'losses' else: return 'draws' - trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False) - # Sell reason + trades = Trade.get_trades([Trade.is_open.is_(False)], include_orders=False) + # Duration + dur: Dict[str, List[float]] = {'wins': [], 'draws': [], 'losses': []} + # Exit reason exit_reasons = {} for trade in trades: if trade.exit_reason not in exit_reasons: exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0} exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1 - # Duration - dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []} - for trade in trades: if trade.close_date is not None and trade.open_date is not None: trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) @@ -422,8 +446,8 @@ class RPC: """ Returns cumulative profit statistics """ trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True)) - trades: List[Trade] = Trade.get_trades( - trade_filter, include_orders=False).order_by(Trade.id).all() + trades: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query( + trade_filter, include_orders=False).order_by(Trade.id)).all() profit_all_coin = [] profit_all_ratio = [] @@ -442,11 +466,11 @@ class RPC: durations.append((trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: - profit_ratio = trade.close_profit - profit_abs = trade.close_profit_abs + profit_ratio = trade.close_profit or 0.0 + profit_abs = trade.close_profit_abs or 0.0 profit_closed_coin.append(profit_abs) profit_closed_ratio.append(profit_ratio) - if trade.close_profit >= 0: + if profit_ratio >= 0: winning_trades += 1 winning_profit += profit_abs else: @@ -499,7 +523,7 @@ class RPC: trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'profit_abs': trade.close_profit_abs} - for trade in trades if not trade.is_open]) + for trade in trades if not trade.is_open and trade.close_date]) max_drawdown_abs = 0.0 max_drawdown = 0.0 if len(trades_df) > 0: @@ -516,9 +540,10 @@ class RPC: fiat_display_currency ) if self._fiat_converter else 0 - first_date = trades[0].open_date if trades else None - last_date = trades[-1].open_date if trades else None + first_date = trades[0].open_date_utc if trades else None + last_date = trades[-1].open_date_utc if trades else None num = float(len(durations) or 1) + bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME) return { 'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), @@ -538,9 +563,11 @@ class RPC: 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), 'closed_trade_count': len([t for t in trades if not t.is_open]), - 'first_trade_date': arrow.get(first_date).humanize() if first_date else '', + 'first_trade_date': first_date.strftime(DATETIME_PRINT_FORMAT) if first_date else '', + 'first_trade_humanized': dt_humanize(first_date) if first_date else '', 'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0, - 'latest_trade_date': arrow.get(last_date).humanize() if last_date else '', + 'latest_trade_date': last_date.strftime(DATETIME_PRINT_FORMAT) if last_date else '', + 'latest_trade_humanized': dt_humanize(last_date) if last_date else '', 'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0, 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_pair': best_pair[0] if best_pair else '', @@ -552,17 +579,48 @@ class RPC: 'max_drawdown': max_drawdown, 'max_drawdown_abs': max_drawdown_abs, 'trading_volume': trading_volume, + 'bot_start_timestamp': int(bot_start.timestamp() * 1000) if bot_start else 0, + 'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '', } + def __balance_get_est_stake( + self, coin: str, stake_currency: str, amount: float, + balance: Wallet, tickers) -> Tuple[float, float]: + est_stake = 0.0 + est_bot_stake = 0.0 + if coin == stake_currency: + est_stake = balance.total + if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + # in Futures, "total" includes the locked stake, and therefore all positions + est_stake = balance.free + est_bot_stake = amount + else: + try: + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) + rate: Optional[float] = tickers.get(pair, {}).get('last', None) + if rate: + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance.total + est_bot_stake = rate * amount + except (ExchangeError): + logger.warning(f"Could not get rate for pair {coin}.") + raise ValueError() + + return est_stake, est_bot_stake + def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ currencies: List[Dict] = [] total = 0.0 + total_bot = 0.0 try: - tickers = self._freqtrade.exchange.get_tickers(cached=True) + tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') + open_trades: List[Trade] = Trade.get_open_trades() + open_assets: Dict[str, Trade] = {t.safe_base_currency: t for t in open_trades} self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() starting_cap_fiat = self._fiat_converter.convert_amount( @@ -573,41 +631,42 @@ class RPC: if not balance.total: continue - est_stake: float = 0 + trade = open_assets.get(coin, None) + is_bot_managed = coin == stake_currency or trade is not None + trade_amount = trade.amount if trade else 0 if coin == stake_currency: - rate = 1.0 - est_stake = balance.total - if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - # in Futures, "total" includes the locked stake, and therefore all positions - est_stake = balance.free - else: - try: - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - rate = tickers.get(pair, {}).get('last') - if rate: - if pair.startswith(stake_currency) and not pair.endswith(stake_currency): - rate = 1.0 / rate - est_stake = rate * balance.total - except (ExchangeError): - logger.warning(f" Could not get rate for pair {coin}.") - continue - total = total + est_stake + trade_amount = self._freqtrade.wallets.get_available_stake_amount() + + try: + est_stake, est_stake_bot = self.__balance_get_est_stake( + coin, stake_currency, trade_amount, balance, tickers) + except ValueError: + continue + + total += est_stake + + if is_bot_managed: + total_bot += est_stake_bot currencies.append({ 'currency': coin, 'free': balance.free, 'balance': balance.total, 'used': balance.used, + 'bot_owned': trade_amount, 'est_stake': est_stake or 0, + 'est_stake_bot': est_stake_bot if is_bot_managed else 0, 'stake': stake_currency, 'side': 'long', 'leverage': 1, 'position': 0, + 'is_bot_managed': is_bot_managed, 'is_position': False, }) symbol: str position: PositionWallet for symbol, position in self._freqtrade.wallets.get_all_positions().items(): total += position.collateral + total_bot += position.collateral currencies.append({ 'currency': symbol, @@ -616,24 +675,30 @@ class RPC: 'used': 0, 'position': position.position, 'est_stake': position.collateral, + 'est_stake_bot': position.collateral, 'stake': stake_currency, 'leverage': position.leverage, 'side': position.side, + 'is_bot_managed': True, 'is_position': True }) value = self._fiat_converter.convert_amount( total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + value_bot = self._fiat_converter.convert_amount( + total_bot, stake_currency, fiat_display_currency) if self._fiat_converter else 0 trade_count = len(Trade.get_trades_proxy()) - starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 - starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 + starting_capital_ratio = (total_bot / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value_bot / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { 'currencies': currencies, 'total': total, + 'total_bot': total_bot, 'symbol': fiat_display_currency, 'value': value, + 'value_bot': value_bot, 'stake': stake_currency, 'starting_capital': starting_capital, 'starting_capital_ratio': starting_capital_ratio, @@ -673,9 +738,22 @@ class RPC: if self._freqtrade.state == State.RUNNING: # Set 'max_open_trades' to 0 self._freqtrade.config['max_open_trades'] = 0 + self._freqtrade.strategy.max_open_trades = 0 return {'status': 'No more entries will occur from now. Run /reload_config to reset.'} + def _rpc_reload_trade_from_exchange(self, trade_id: int) -> Dict[str, str]: + """ + Handler for reload_trade_from_exchange. + Reloads a trade from it's orders, should manual interaction have happened. + """ + trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() + if not trade: + raise RPCException(f"Could not find trade with id {trade_id}.") + + self._freqtrade.handle_onexchange_order(trade) + return {'status': 'Reloaded from orders from exchange'} + def __exec_force_exit(self, trade: Trade, ordertype: Optional[str], amount: Optional[float] = None) -> None: # Check if there is there is an open order @@ -777,7 +855,8 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() + trade: Optional[Trade] = Trade.get_trades( + [Trade.is_open.is_(True), Trade.pair == pair]).first() is_short = (order_side == SignalDirection.SHORT) if trade: is_short = trade.is_short @@ -811,6 +890,29 @@ class RPC: else: raise RPCException(f'Failed to enter position for {pair}.') + def _rpc_cancel_open_order(self, trade_id: int): + if self._freqtrade.state != State.RUNNING: + raise RPCException('trader is not running') + with self._freqtrade._exit_lock: + # Query for trade + trade = Trade.get_trades( + trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] + ).first() + if not trade: + logger.warning('cancel_open_order: Invalid trade_id received.') + raise RPCException('Invalid trade_id.') + if not trade.open_order_id: + logger.warning('cancel_open_order: No open order for trade_id.') + raise RPCException('No open order for trade_id.') + + try: + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) + except ExchangeError as e: + logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True) + raise RPCException("Order not found.") + self._freqtrade.handle_cancel_order(order, trade, CANCEL_REASON['USER_CANCEL']) + Trade.commit() + def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: """ Handler for delete . @@ -907,12 +1009,12 @@ class RPC: def _rpc_delete_lock(self, lockid: Optional[int] = None, pair: Optional[str] = None) -> Dict[str, Any]: """ Delete specific lock(s) """ - locks = [] + locks: Sequence[PairLock] = [] if pair: locks = PairLocks.get_pair_locks(pair) if lockid: - locks = PairLock.query.filter(PairLock.id == lockid).all() + locks = PairLock.session.scalars(select(PairLock).filter(PairLock.id == lockid)).all() for lock in locks: lock.active = False @@ -944,7 +1046,7 @@ class RPC: resp['errors'] = errors return resp - def _rpc_blacklist(self, add: List[str] = None) -> Dict: + def _rpc_blacklist(self, add: Optional[List[str]] = None) -> Dict: """ Returns the currently active blacklist""" errors = {} if add: @@ -1126,12 +1228,12 @@ class RPC: return self._freqtrade.active_pair_whitelist @staticmethod - def _rpc_analysed_history_full(config, pair: str, timeframe: str, - timerange: str, exchange) -> Dict[str, Any]: - timerange_parsed = TimeRange.parse_timerange(timerange) + def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str, + exchange) -> Dict[str, Any]: + timerange_parsed = TimeRange.parse_timerange(config.get('timerange')) _data = load_data( - datadir=config.get("datadir"), + datadir=config["datadir"], pairs=[pair], timeframe=timeframe, timerange=timerange_parsed, @@ -1139,16 +1241,18 @@ class RPC: candle_type=config.get('candle_type_def', CandleType.SPOT) ) if pair not in _data: - raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.") + raise RPCException( + f"No data for {pair}, {timeframe} in {config.get('timerange')} found.") from freqtrade.data.dataprovider import DataProvider from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(config) strategy.dp = DataProvider(config, exchange=exchange, pairlists=None) + strategy.ft_bot_start() df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, - df_analyzed, arrow.Arrow.utcnow().datetime) + df_analyzed, dt_now()) def _rpc_plot_config(self) -> Dict[str, Any]: if (self._freqtrade.strategy.plot_config and @@ -1156,6 +1260,16 @@ class RPC: self._freqtrade.strategy.plot_config['subplots'] = {} return self._freqtrade.strategy.plot_config + @staticmethod + def _rpc_plot_config_with_strategy(config: Config) -> Dict[str, Any]: + + from freqtrade.resolvers.strategy_resolver import StrategyResolver + strategy = StrategyResolver.load_strategy(config) + + if (strategy.plot_config and 'subplots' not in strategy.plot_config): + strategy.plot_config['subplots'] = {} + return strategy.plot_config + @staticmethod def _rpc_sysinfo() -> Dict[str, Any]: return { @@ -1163,10 +1277,23 @@ class RPC: "ram_pct": psutil.virtual_memory().percent } - def _health(self) -> Dict[str, Union[str, int]]: + def health(self) -> Dict[str, Optional[Union[str, int]]]: last_p = self._freqtrade.last_process + if last_p is None: + return { + "last_process": None, + "last_process_loc": None, + "last_process_ts": None, + } + return { - 'last_process': str(last_p), - 'last_process_loc': last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT), - 'last_process_ts': int(last_p.timestamp()), + "last_process": str(last_p), + "last_process_loc": last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT), + "last_process_ts": int(last_p.timestamp()), } + + def _update_market_direction(self, direction: MarketDirection) -> None: + self._freqtrade.strategy.market_direction = direction + + def _get_market_direction(self) -> MarketDirection: + return self._freqtrade.strategy.market_direction diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index c4d4fa2dd..1972ad6e5 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -3,11 +3,12 @@ This module contains class to manage RPC communications (Telegram, API, ...) """ import logging from collections import deque -from typing import Any, Dict, List +from typing import List from freqtrade.constants import Config from freqtrade.enums import NO_ECHO_MESSAGES, RPCMessageType from freqtrade.rpc import RPC, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -58,7 +59,7 @@ class RPCManager: mod.cleanup() del mod - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send given message to all registered rpc modules. A message consists of one or more key value pairs of strings. @@ -69,10 +70,6 @@ class RPCManager: """ if msg.get('type') not in NO_ECHO_MESSAGES: logger.info('Sending rpc message: %s', msg) - if 'pair' in msg: - msg.update({ - 'base_currency': self._rpc._freqtrade.exchange.get_pair_base_currency(msg['pair']) - }) for mod in self.registered_modules: logger.debug('Forwarding message to rpc.%s', mod.name) try: diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py new file mode 100644 index 000000000..23f3ed5a9 --- /dev/null +++ b/freqtrade/rpc/rpc_types.py @@ -0,0 +1,128 @@ +from datetime import datetime +from typing import Any, List, Literal, Optional, TypedDict, Union + +from freqtrade.constants import PairWithTimeframe +from freqtrade.enums import RPCMessageType + + +class RPCSendMsgBase(TypedDict): + pass + # ty1pe: Literal[RPCMessageType] + + +class RPCStatusMsg(RPCSendMsgBase): + """Used for Status, Startup and Warning messages""" + type: Literal[RPCMessageType.STATUS, RPCMessageType.STARTUP, RPCMessageType.WARNING] + status: str + + +class RPCStrategyMsg(RPCSendMsgBase): + """Used for Status, Startup and Warning messages""" + type: Literal[RPCMessageType.STRATEGY_MSG] + msg: str + + +class RPCProtectionMsg(RPCSendMsgBase): + type: Literal[RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL] + id: int + pair: str + base_currency: Optional[str] + lock_time: str + lock_timestamp: int + lock_end_time: str + lock_end_timestamp: int + reason: str + side: str + active: bool + + +class RPCWhitelistMsg(RPCSendMsgBase): + type: Literal[RPCMessageType.WHITELIST] + data: List[str] + + +class __RPCBuyMsgBase(RPCSendMsgBase): + trade_id: int + buy_tag: Optional[str] + enter_tag: Optional[str] + exchange: str + pair: str + base_currency: str + leverage: Optional[float] + direction: str + limit: float + open_rate: float + order_type: str + stake_amount: float + stake_currency: str + fiat_currency: Optional[str] + amount: float + open_date: datetime + current_rate: Optional[float] + sub_trade: bool + + +class RPCBuyMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL] + + +class RPCCancelMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.ENTRY_CANCEL] + reason: str + + +class RPCSellMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.EXIT, RPCMessageType.EXIT_FILL] + cumulative_profit: float + gain: str # Literal["profit", "loss"] + close_rate: float + profit_amount: float + profit_ratio: float + sell_reason: Optional[str] + exit_reason: Optional[str] + close_date: datetime + # current_rate: Optional[float] + order_rate: Optional[float] + + +class RPCSellCancelMsg(__RPCBuyMsgBase): + type: Literal[RPCMessageType.EXIT_CANCEL] + reason: str + gain: str # Literal["profit", "loss"] + profit_amount: float + profit_ratio: float + sell_reason: Optional[str] + exit_reason: Optional[str] + close_date: datetime + + +class _AnalyzedDFData(TypedDict): + key: PairWithTimeframe + df: Any + la: datetime + + +class RPCAnalyzedDFMsg(RPCSendMsgBase): + """New Analyzed dataframe message""" + type: Literal[RPCMessageType.ANALYZED_DF] + data: _AnalyzedDFData + + +class RPCNewCandleMsg(RPCSendMsgBase): + """New candle ping message, issued once per new candle/pair""" + type: Literal[RPCMessageType.NEW_CANDLE] + data: PairWithTimeframe + + +RPCSendMsg = Union[ + RPCStatusMsg, + RPCStrategyMsg, + RPCProtectionMsg, + RPCWhitelistMsg, + RPCBuyMsg, + RPCCancelMsg, + RPCSellMsg, + RPCSellCancelMsg, + RPCAnalyzedDFMsg, + RPCNewCandleMsg + ] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 38fe0cd13..d082299cb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ +import asyncio import json import logging import re @@ -13,23 +14,29 @@ from functools import partial from html import escape from itertools import chain from math import isnan -from typing import Any, Callable, Dict, List, Optional, Union +from threading import Thread +from typing import Any, Callable, Coroutine, Dict, List, Optional, Union -import arrow from tabulate import tabulate -from telegram import (MAX_MESSAGE_LENGTH, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, - KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update) +from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, + ReplyKeyboardMarkup, Update) +from telegram.constants import MessageLimit, ParseMode from telegram.error import BadRequest, NetworkError, TelegramError -from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater -from telegram.utils.helpers import escape_markdown +from telegram.ext import Application, CallbackContext, CallbackQueryHandler, CommandHandler +from telegram.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN, Config -from freqtrade.enums import RPCMessageType, SignalDirection, TradingMode +from freqtrade.enums import MarketDirection, RPCMessageType, SignalDirection, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg +from freqtrade.util import dt_humanize + + +MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH logger = logging.getLogger(__name__) @@ -46,14 +53,14 @@ class TimeunitMappings: default: int -def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: +def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): """ Decorator to check if the message comes from the correct chat_id :param command_handler: Telegram CommandHandler :return: decorated function """ - def wrapper(self, *args, **kwargs): + async def wrapper(self, *args, **kwargs): """ Decorator logic """ update = kwargs.get('update') or args[0] @@ -65,10 +72,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: chat_id = int(self._config['telegram']['chat_id']) if cchat_id != chat_id: - logger.info( - 'Rejected unauthorized message from: %s', - update.message.chat_id - ) + logger.info(f'Rejected unauthorized message from: {update.message.chat_id}') return wrapper # Rollback session to avoid getting data stored in a transaction. Trade.rollback() @@ -78,11 +82,13 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: chat_id ) try: - return command_handler(self, *args, **kwargs) + return await command_handler(self, *args, **kwargs) except RPCException as e: - self._send_msg(str(e)) + await self._send_msg(str(e)) except BaseException: logger.exception('Exception occurred within Telegram module') + finally: + Trade.session.remove() return wrapper @@ -99,9 +105,17 @@ class Telegram(RPCHandler): """ super().__init__(rpc, config) - self._updater: Updater + self._app: Application + self._loop: asyncio.AbstractEventLoop self._init_keyboard() - self._init() + self._start_thread() + + def _start_thread(self): + """ + Creates and starts the polling thread + """ + self._thread = Thread(target=self._init, name='FTTelegram') + self._thread.start() def _init_keyboard(self) -> None: """ @@ -129,7 +143,8 @@ class Telegram(RPCHandler): r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/forcebuy$', r'/forcelong$', r'/forceshort$', r'/forcesell$', r'/forceexit$', - r'/edge$', r'/health$', r'/help$', r'/version$' + r'/edge$', r'/health$', r'/help$', r'/version$', r'/marketdir (long|short|even|none)$', + r'/marketdir$' ] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -151,14 +166,23 @@ class Telegram(RPCHandler): logger.info('using custom keyboard from ' f'config.json: {self._keyboard}') + def _init_telegram_app(self): + return Application.builder().token(self._config['telegram']['token']).build() + def _init(self) -> None: """ Initializes this module with the given config, registers all known command handlers and starts polling for message updates + Runs in a separate thread. """ - self._updater = Updater(token=self._config['telegram']['token'], workers=0, - use_context=True) + try: + self._loop = asyncio.get_running_loop() + except RuntimeError: + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + + self._app = self._init_telegram_app() # Register command handler and start telegram message polling handles = [ @@ -172,8 +196,10 @@ class Telegram(RPCHandler): self._force_enter, order_side=SignalDirection.LONG)), CommandHandler('forceshort', partial( self._force_enter, order_side=SignalDirection.SHORT)), + CommandHandler('reload_trade', self._reload_trade_from_exchange), CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), + CommandHandler(['coo', 'cancel_open_order'], self._cancel_open_order), CommandHandler('performance', self._performance), CommandHandler(['buys', 'entries'], self._enter_tag_performance), CommandHandler(['sells', 'exits'], self._exit_reason_performance), @@ -196,6 +222,7 @@ class Telegram(RPCHandler): CommandHandler('health', self._health), CommandHandler('help', self._help), CommandHandler('version', self._version), + CommandHandler('marketdir', self._changemarketdir) ] callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), @@ -215,21 +242,38 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"), ] for handle in handles: - self._updater.dispatcher.add_handler(handle) + self._app.add_handler(handle) for callback in callbacks: - self._updater.dispatcher.add_handler(callback) + self._app.add_handler(callback) - self._updater.start_polling( - bootstrap_retries=-1, - timeout=20, - read_latency=60, # Assumed transmission latency - drop_pending_updates=True, - ) logger.info( 'rpc.telegram is listening for following commands: %s', - [h.command for h in handles] + [[x for x in sorted(h.commands)] for h in handles] ) + self._loop.run_until_complete(self._startup_telegram()) + + async def _startup_telegram(self) -> None: + await self._app.initialize() + await self._app.start() + if self._app.updater: + await self._app.updater.start_polling( + bootstrap_retries=-1, + timeout=20, + # read_latency=60, # Assumed transmission latency + drop_pending_updates=True, + # stop_signals=[], # Necessary as we don't run on the main thread + ) + while True: + await asyncio.sleep(10) + if not self._app.updater.running: + break + + async def _cleanup_telegram(self) -> None: + if self._app.updater: + await self._app.updater.stop() + await self._app.stop() + await self._app.shutdown() def cleanup(self) -> None: """ @@ -237,7 +281,8 @@ class Telegram(RPCHandler): :return: None """ # This can take up to `timeout` from the call to `start_polling`. - self._updater.stop() + asyncio.run_coroutine_threadsafe(self._cleanup_telegram(), self._loop) + self._thread.join() def _exchange_from_msg(self, msg: Dict[str, Any]) -> str: """ @@ -318,31 +363,33 @@ class Telegram(RPCHandler): and self._rpc._fiat_converter): msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - msg['profit_extra'] = ( - f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}") + msg['profit_extra'] = f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}" else: msg['profit_extra'] = '' msg['profit_extra'] = ( f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" f"{msg['profit_extra']})") + is_fill = msg['type'] == RPCMessageType.EXIT_FILL is_sub_trade = msg.get('sub_trade') is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit') - profit_prefix = ('Sub ' if is_sub_profit - else 'Cumulative ') if is_sub_trade else '' + profit_prefix = ('Sub ' if is_sub_profit else 'Cumulative ') if is_sub_trade else '' cp_extra = '' + exit_wording = 'Exited' if is_fill else 'Exiting' if is_sub_profit and is_sub_trade: if self._rpc._fiat_converter: cp_fiat = self._rpc._fiat_converter.convert_amount( msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency']) cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}" - else: - cp_extra = '' - cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \ - f"{msg['stake_currency']}{cp_extra}`)\n" + exit_wording = f"Partially {exit_wording.lower()}" + cp_extra = ( + f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " + f"{msg['stake_currency']}{cp_extra}`)\n" + ) + message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " - f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" + f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n" f"{self._add_analyzed_candle(msg['pair'])}" f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" @@ -361,7 +408,7 @@ class Telegram(RPCHandler): elif msg['type'] == RPCMessageType.EXIT_FILL: message += f"*Exit Rate:* `{msg['close_rate']:.8f}`" - if msg.get('sub_trade'): + if is_sub_trade: if self._rpc._fiat_converter: msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) @@ -409,6 +456,9 @@ class Telegram(RPCHandler): elif msg_type == RPCMessageType.WARNING: message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`" + elif msg_type == RPCMessageType.EXCEPTION: + # Errors will contain exceptions, which are wrapped in tripple ticks. + message = f"\N{WARNING SIGN} *ERROR:* \n {msg['status']}" elif msg_type == RPCMessageType.STARTUP: message = f"{msg['status']}" @@ -419,14 +469,14 @@ class Telegram(RPCHandler): return None return message - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send a message to telegram channel """ default_noti = 'on' msg_type = msg['type'] noti = '' - if msg_type == RPCMessageType.EXIT: + if msg['type'] == RPCMessageType.EXIT: sell_noti = self._config['telegram'] \ .get('notification_settings', {}).get(str(msg_type), {}) # For backward compatibility sell still can be string @@ -443,9 +493,11 @@ class Telegram(RPCHandler): # Notification disabled return - message = self.compose_message(deepcopy(msg), msg_type) + message = self.compose_message(deepcopy(msg), msg_type) # type: ignore if message: - self._send_msg(message, disable_notification=(noti == 'silent')) + asyncio.run_coroutine_threadsafe( + self._send_msg(message, disable_notification=(noti == 'silent')), + self._loop) def _get_sell_emoji(self, msg): """ @@ -468,59 +520,58 @@ class Telegram(RPCHandler): lines_detail: List[str] = [] if len(filled_orders) > 0: first_avg = filled_orders[0]["safe_price"] - - for x, order in enumerate(filled_orders): + order_nr = 0 + for order in filled_orders: lines: List[str] = [] if order['is_open'] is True: continue + order_nr += 1 wording = 'Entry' if order['ft_is_entry'] else 'Exit' - cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_amount = order["filled"] or order["amount"] cur_entry_average = order["safe_price"] lines.append(" ") - if x == 0: - lines.append(f"*{wording} #{x+1}:*") + if order_nr == 1: + lines.append(f"*{wording} #{order_nr}:*") lines.append( - f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + f"*Amount:* {cur_entry_amount} " + f"({round_coin_value(order['cost'], quote_currency)})" + ) lines.append(f"*Average Price:* {cur_entry_average}") else: - sumA = 0 - sumB = 0 - for y in range(x): - amount = filled_orders[y]["filled"] or filled_orders[y]["amount"] - sumA += amount * filled_orders[y]["safe_price"] - sumB += amount - prev_avg_price = sumA / sumB + sum_stake = 0 + sum_amount = 0 + for y in range(order_nr): + loc_order = filled_orders[y] + if loc_order['is_open'] is True: + # Skip open orders (e.g. stop orders) + continue + amount = loc_order["filled"] or loc_order["amount"] + sum_stake += amount * loc_order["safe_price"] + sum_amount += amount + prev_avg_price = sum_stake / sum_amount # TODO: This calculation ignores fees. price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) minus_on_entry = 0 if prev_avg_price: minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price - lines.append(f"*{wording} #{x+1}:* at {minus_on_entry:.2%} avg profit") + lines.append(f"*{wording} #{order_nr}:* at {minus_on_entry:.2%} avg Profit") if is_open: - lines.append("({})".format(cur_entry_datetime - .humanize(granularity=["day", "hour", "minute"]))) - lines.append( - f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + lines.append("({})".format(dt_humanize(order["order_filled_date"], + granularity=["day", "hour", "minute"]))) + lines.append(f"*Amount:* {cur_entry_amount} " + f"({round_coin_value(order['cost'], quote_currency)})") lines.append(f"*Average {wording} Price:* {cur_entry_average} " - f"({price_to_1st_entry:.2%} from 1st entry rate)") + f"({price_to_1st_entry:.2%} from 1st entry Rate)") lines.append(f"*Order filled:* {order['order_filled_date']}") - # TODO: is this really useful? - # dur_entry = cur_entry_datetime - arrow.get( - # filled_orders[x - 1]["order_filled_date"]) - # days = dur_entry.days - # hours, remainder = divmod(dur_entry.seconds, 3600) - # minutes, seconds = divmod(remainder, 60) - # lines.append( - # f"({days}d {hours}h {minutes}m {seconds}s from previous {wording.lower()})") lines_detail.append("\n".join(lines)) + return lines_detail @authorized_only - def _status(self, update: Update, context: CallbackContext) -> None: + async def _status(self, update: Update, context: CallbackContext) -> None: """ Handler for /status. Returns the current TradeThread status @@ -530,12 +581,12 @@ class Telegram(RPCHandler): """ if context.args and 'table' in context.args: - self._status_table(update, context) + await self._status_table(update, context) return else: - self._status_msg(update, context) + await self._status_msg(update, context) - def _status_msg(self, update: Update, context: CallbackContext) -> None: + async def _status_msg(self, update: Update, context: CallbackContext) -> None: """ handler for `/status` and `/status `. @@ -550,37 +601,56 @@ class Telegram(RPCHandler): position_adjust = self._config.get('position_adjustment_enable', False) max_entries = self._config.get('max_entry_position_adjustment', -1) for r in results: - r['open_date_hum'] = arrow.get(r['open_date']).humanize() + r['open_date_hum'] = dt_humanize(r['open_date']) r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) + r['num_exits'] = len([o for o in r['orders'] if not o['ft_is_entry'] + and not o['ft_order_side'] == 'stoploss']) r['exit_reason'] = r.get('exit_reason', "") + r['stake_amount_r'] = round_coin_value(r['stake_amount'], r['quote_currency']) + r['max_stake_amount_r'] = round_coin_value( + r['max_stake_amount'] or r['stake_amount'], r['quote_currency']) + r['profit_abs_r'] = round_coin_value(r['profit_abs'], r['quote_currency']) + r['realized_profit_r'] = round_coin_value(r['realized_profit'], r['quote_currency']) + r['total_profit_abs_r'] = round_coin_value( + r['total_profit_abs'], r['quote_currency']) lines = [ "*Trade ID:* `{trade_id}`" + (" `(since {open_date_hum})`" if r['is_open'] else ""), "*Current Pair:* {pair}", - "*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"), - "*Leverage:* `{leverage}`" if r.get('leverage') else "", - "*Amount:* `{amount} ({stake_amount} {quote_currency})`", + f"*Direction:* {'`Short`' if r.get('is_short') else '`Long`'}" + + " ` ({leverage}x)`" if r.get('leverage') else "", + "*Amount:* `{amount} ({stake_amount_r})`", + "*Total invested:* `{max_stake_amount_r}`" if position_adjust else "", "*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "", "*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "", ] if position_adjust: max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") - lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str) + lines.extend([ + "*Number of Entries:* `{num_entries}" + max_buy_str + "`", + "*Number of Exits:* `{num_exits}`" + ]) lines.extend([ "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "", "*Open Date:* `{open_date}`", "*Close Date:* `{close_date}`" if r['close_date'] else "", - "*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", - ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") - + "`{profit_ratio:.2%}`", + " \n*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", + ("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *") + + "`{profit_ratio:.2%}` `({profit_abs_r})`", ]) if r['is_open']: if r.get('realized_profit'): - lines.append("*Realized Profit:* `{realized_profit:.8f}`") + lines.extend([ + "*Realized Profit:* `{realized_profit_ratio:.2%} ({realized_profit_r})`", + "*Total Profit:* `{total_profit_ratio:.2%} ({total_profit_abs_r})`" + ]) + + # Append empty line to improve readability + lines.append(" ") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss @@ -600,9 +670,9 @@ class Telegram(RPCHandler): lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) lines.extend(lines_detail if lines_detail else "") - self.__send_status_msg(lines, r) + await self.__send_status_msg(lines, r) - def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: + async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: """ Send status message. """ @@ -613,13 +683,13 @@ class Telegram(RPCHandler): if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: msg += line + '\n' else: - self._send_msg(msg.format(**r)) + await self._send_msg(msg.format(**r)) msg = "*Trade ID:* `{trade_id}` - continued\n" + line + '\n' - self._send_msg(msg.format(**r)) + await self._send_msg(msg.format(**r)) @authorized_only - def _status_table(self, update: Update, context: CallbackContext) -> None: + async def _status_table(self, update: Update, context: CallbackContext) -> None: """ Handler for /status table. Returns the current TradeThread status in table format @@ -652,12 +722,11 @@ class Telegram(RPCHandler): # insert separators line between Total lines = message.split("\n") message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]]) - self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_status_table", - query=update.callback_query) + await self._send_msg(f"
{message}
", parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_status_table", + query=update.callback_query) - @authorized_only - def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: + async def _timeunit_stats(self, update: Update, context: CallbackContext, unit: str) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. @@ -704,11 +773,11 @@ class Telegram(RPCHandler): f'{val.message} Profit over the last {timescale} {val.message2}:\n' f'
{stats_tab}
' ) - self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, - callback_path=val.callback, query=update.callback_query) + await self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path=val.callback, query=update.callback_query) @authorized_only - def _daily(self, update: Update, context: CallbackContext) -> None: + async def _daily(self, update: Update, context: CallbackContext) -> None: """ Handler for /daily Returns a daily profit (in BTC) over the last n days. @@ -716,10 +785,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'days') + await self._timeunit_stats(update, context, 'days') @authorized_only - def _weekly(self, update: Update, context: CallbackContext) -> None: + async def _weekly(self, update: Update, context: CallbackContext) -> None: """ Handler for /weekly Returns a weekly profit (in BTC) over the last n weeks. @@ -727,10 +796,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'weeks') + await self._timeunit_stats(update, context, 'weeks') @authorized_only - def _monthly(self, update: Update, context: CallbackContext) -> None: + async def _monthly(self, update: Update, context: CallbackContext) -> None: """ Handler for /monthly Returns a monthly profit (in BTC) over the last n months. @@ -738,10 +807,10 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - self._timeunit_stats(update, context, 'months') + await self._timeunit_stats(update, context, 'months') @authorized_only - def _profit(self, update: Update, context: CallbackContext) -> None: + async def _profit(self, update: Update, context: CallbackContext) -> None: """ Handler for /profit. Returns a cumulative profit statistics. @@ -775,13 +844,13 @@ class Telegram(RPCHandler): profit_all_percent = stats['profit_all_percent'] profit_all_fiat = stats['profit_all_fiat'] trade_count = stats['trade_count'] - first_trade_date = stats['first_trade_date'] - latest_trade_date = stats['latest_trade_date'] + first_trade_date = f"{stats['first_trade_humanized']} ({stats['first_trade_date']})" + latest_trade_date = f"{stats['latest_trade_humanized']} ({stats['latest_trade_date']})" avg_duration = stats['avg_duration'] best_pair = stats['best_pair'] best_pair_profit_ratio = stats['best_pair_profit_ratio'] if stats['trade_count'] == 0: - markdown_msg = 'No trades yet.' + markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" else: # Message to display if stats['closed_trade_count'] > 0: @@ -800,6 +869,7 @@ class Telegram(RPCHandler): f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" f"*Total Trade Count:* `{trade_count}`\n" + f"*Bot started:* `{stats['bot_start_date']}`\n" f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " f"`{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}`\n" @@ -814,11 +884,11 @@ class Telegram(RPCHandler): f"*Max Drawdown:* `{stats['max_drawdown']:.2%} " f"({round_coin_value(stats['max_drawdown_abs'], stake_cur)})`" ) - self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", - query=update.callback_query) + await self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", + query=update.callback_query) @authorized_only - def _stats(self, update: Update, context: CallbackContext) -> None: + async def _stats(self, update: Update, context: CallbackContext) -> None: """ Handler for /stats Show stats of recent trades @@ -849,7 +919,7 @@ class Telegram(RPCHandler): headers=['Exit Reason', 'Exits', 'Wins', 'Losses'] ) if len(exit_reasons_tabulate) > 25: - self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) + await self._send_msg(f"```\n{exit_reasons_msg}```", ParseMode.MARKDOWN) exit_reasons_msg = '' durations = stats['durations'] @@ -864,11 +934,12 @@ class Telegram(RPCHandler): ) msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""") - self._send_msg(msg, ParseMode.MARKDOWN) + await self._send_msg(msg, ParseMode.MARKDOWN) @authorized_only - def _balance(self, update: Update, context: CallbackContext) -> None: + async def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ + full_result = context.args and 'full' in context.args result = self._rpc._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) @@ -879,8 +950,7 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: output += "*Warning:* Simulated balances in Dry Mode.\n" - starting_cap = round_coin_value( - result['starting_capital'], self._config['stake_currency']) + starting_cap = round_coin_value(result['starting_capital'], self._config['stake_currency']) output += f"Starting capital: `{starting_cap}`" starting_cap_fiat = round_coin_value( result['starting_capital_fiat'], self._config['fiat_display_currency'] @@ -892,7 +962,10 @@ class Telegram(RPCHandler): total_dust_currencies = 0 for curr in result['currencies']: curr_output = '' - if curr['est_stake'] > balance_dust_level: + if ( + (curr['is_position'] or curr['est_stake'] > balance_dust_level) + and (full_result or curr['is_bot_managed']) + ): if curr['is_position']: curr_output = ( f"*{curr['currency']}:*\n" @@ -901,20 +974,24 @@ class Telegram(RPCHandler): f"\t`Est. {curr['stake']}: " f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: + est_stake = round_coin_value( + curr['est_stake' if full_result else 'est_stake_bot'], curr['stake'], False) + curr_output = ( f"*{curr['currency']}:*\n" f"\t`Available: {curr['free']:.8f}`\n" f"\t`Balance: {curr['balance']:.8f}`\n" f"\t`Pending: {curr['used']:.8f}`\n" - f"\t`Est. {curr['stake']}: " - f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") + f"\t`Bot Owned: {curr['bot_owned']:.8f}`\n" + f"\t`Est. {curr['stake']}: {est_stake}`\n") + elif curr['est_stake'] <= balance_dust_level: total_dust_balance += curr['est_stake'] total_dust_currencies += 1 # Handle overflowing message length if len(output + curr_output) >= MAX_MESSAGE_LENGTH: - self._send_msg(output) + await self._send_msg(output) output = curr_output else: output += curr_output @@ -929,19 +1006,20 @@ class Telegram(RPCHandler): tc = result['trade_count'] > 0 stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else '' fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else '' - - output += ("\n*Estimated Value*:\n" - f"\t`{result['stake']}: " - f"{round_coin_value(result['total'], result['stake'], False)}`" - f"{stake_improve}\n" - f"\t`{result['symbol']}: " - f"{round_coin_value(result['value'], result['symbol'], False)}`" - f"{fiat_val}\n") - self._send_msg(output, reload_able=True, callback_path="update_balance", - query=update.callback_query) + value = round_coin_value( + result['value' if full_result else 'value_bot'], result['symbol'], False) + total_stake = round_coin_value( + result['total' if full_result else 'total_bot'], result['stake'], False) + output += ( + f"\n*Estimated Value{' (Bot managed assets only)' if not full_result else ''}*:\n" + f"\t`{result['stake']}: {total_stake}`{stake_improve}\n" + f"\t`{result['symbol']}: {value}`{fiat_val}\n" + ) + await self._send_msg(output, reload_able=True, callback_path="update_balance", + query=update.callback_query) @authorized_only - def _start(self, update: Update, context: CallbackContext) -> None: + async def _start(self, update: Update, context: CallbackContext) -> None: """ Handler for /start. Starts TradeThread @@ -950,10 +1028,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_start() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _stop(self, update: Update, context: CallbackContext) -> None: + async def _stop(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop. Stops TradeThread @@ -962,10 +1040,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_stop() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _reload_config(self, update: Update, context: CallbackContext) -> None: + async def _reload_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /reload_config. Triggers a config file reload @@ -974,10 +1052,10 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_reload_config() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _stopentry(self, update: Update, context: CallbackContext) -> None: + async def _stopentry(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop_buy. Sets max_open_trades to 0 and gracefully sells all open trades @@ -986,10 +1064,21 @@ class Telegram(RPCHandler): :return: None """ msg = self._rpc._rpc_stopentry() - self._send_msg(f"Status: `{msg['status']}`") + await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - def _force_exit(self, update: Update, context: CallbackContext) -> None: + async def _reload_trade_from_exchange(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /reload_trade . + """ + if not context.args or len(context.args) == 0: + raise RPCException("Trade-id not set.") + trade_id = int(context.args[0]) + msg = self._rpc._rpc_reload_trade_from_exchange(trade_id) + await self._send_msg(f"Status: `{msg['status']}`") + + @authorized_only + async def _force_exit(self, update: Update, context: CallbackContext) -> None: """ Handler for /forceexit . Sells the given trade at current price @@ -1000,14 +1089,14 @@ class Telegram(RPCHandler): if context.args: trade_id = context.args[0] - self._force_exit_action(trade_id) + await self._force_exit_action(trade_id) else: fiat_currency = self._config.get('fiat_display_currency', '') try: statlist, _, _ = self._rpc._rpc_status_table( self._config['stake_currency'], fiat_currency) except RPCException: - self._send_msg(msg='No open trade found.') + await self._send_msg(msg='No open trade found.') return trades = [] for trade in statlist: @@ -1020,47 +1109,51 @@ class Telegram(RPCHandler): buttons_aligned.append([InlineKeyboardButton( text='Cancel', callback_data='force_exit__cancel')]) - self._send_msg(msg="Which trade?", keyboard=buttons_aligned) + await self._send_msg(msg="Which trade?", keyboard=buttons_aligned) - def _force_exit_action(self, trade_id): + async def _force_exit_action(self, trade_id): if trade_id != 'cancel': try: self._rpc._rpc_force_exit(trade_id) except RPCException as e: - self._send_msg(str(e)) + await self._send_msg(str(e)) - def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: + async def _force_exit_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query if query.data and '__' in query.data: # Input data is "force_exit__" trade_id = query.data.split("__")[1].split(' ')[0] if trade_id == 'cancel': - query.answer() - query.edit_message_text(text="Force exit canceled.") + await query.answer() + await query.edit_message_text(text="Force exit canceled.") return - trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first() - query.answer() - query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}") - self._force_exit_action(trade_id) + trade: Optional[Trade] = Trade.get_trades(trade_filter=Trade.id == trade_id).first() + await query.answer() + if trade: + await query.edit_message_text( + text=f"Manually exiting Trade #{trade_id}, {trade.pair}") + await self._force_exit_action(trade_id) + else: + await query.edit_message_text(text=f"Trade {trade_id} not found.") - def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): + async def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection): if pair != 'cancel': try: self._rpc._rpc_force_entry(pair, price, order_side=order_side) except RPCException as e: logger.exception("Forcebuy error!") - self._send_msg(str(e), ParseMode.HTML) + await self._send_msg(str(e), ParseMode.HTML) - def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: + async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query if query.data and '_||_' in query.data: pair, side = query.data.split('_||_') order_side = SignalDirection(side) - query.answer() - query.edit_message_text(text=f"Manually entering {order_side} for {pair}") - self._force_enter_action(pair, None, order_side) + await query.answer() + await query.edit_message_text(text=f"Manually entering {order_side} for {pair}") + await self._force_enter_action(pair, None, order_side) @staticmethod def _layout_inline_keyboard( @@ -1073,7 +1166,7 @@ class Telegram(RPCHandler): return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @authorized_only - def _force_enter( + async def _force_enter( self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None: """ Handler for /forcelong and `/forceshort @@ -1085,7 +1178,7 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None - self._force_enter_action(pair, price, order_side) + await self._force_enter_action(pair, price, order_side) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] pair_buttons = [ @@ -1095,12 +1188,12 @@ class Telegram(RPCHandler): buttons_aligned = self._layout_inline_keyboard(pair_buttons) buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) - self._send_msg(msg="Which pair?", - keyboard=buttons_aligned, - query=update.callback_query) + await self._send_msg(msg="Which pair?", + keyboard=buttons_aligned, + query=update.callback_query) @authorized_only - def _trades(self, update: Update, context: CallbackContext) -> None: + async def _trades(self, update: Update, context: CallbackContext) -> None: """ Handler for /trades Returns last n recent trades. @@ -1117,7 +1210,7 @@ class Telegram(RPCHandler): nrecent ) trades_tab = tabulate( - [[arrow.get(trade['close_date']).humanize(), + [[dt_humanize(trade['close_date']), trade['pair'] + " (#" + str(trade['trade_id']) + ")", f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})"] for trade in trades['trades']], @@ -1129,10 +1222,10 @@ class Telegram(RPCHandler): tablefmt='simple') message = (f"{min(trades['trades_count'], nrecent)} recent trades:\n" + (f"
{trades_tab}
" if trades['trades_count'] > 0 else '')) - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _delete_trade(self, update: Update, context: CallbackContext) -> None: + async def _delete_trade(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete . Delete the given trade @@ -1144,13 +1237,28 @@ class Telegram(RPCHandler): raise RPCException("Trade-id not set.") trade_id = int(context.args[0]) msg = self._rpc._rpc_delete(trade_id) - self._send_msg(( + await self._send_msg( f"`{msg['result_msg']}`\n" 'Please make sure to take care of this asset on the exchange manually.' - )) + ) @authorized_only - def _performance(self, update: Update, context: CallbackContext) -> None: + async def _cancel_open_order(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /cancel_open_order . + Cancel open order for tradeid + :param bot: telegram bot + :param update: message update + :return: None + """ + if not context.args or len(context.args) == 0: + raise RPCException("Trade-id not set.") + trade_id = int(context.args[0]) + self._rpc._rpc_cancel_open_order(trade_id) + await self._send_msg('Open order canceled.') + + @authorized_only + async def _performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /performance. Shows a performance statistic from finished trades @@ -1168,17 +1276,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_performance", + query=update.callback_query) @authorized_only - def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: + async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /buys PAIR . Shows a performance statistic from finished trades @@ -1200,17 +1308,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_enter_tag_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_enter_tag_performance", + query=update.callback_query) @authorized_only - def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: + async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /sells. Shows a performance statistic from finished trades @@ -1232,17 +1340,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_exit_reason_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_exit_reason_performance", + query=update.callback_query) @authorized_only - def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: + async def _mix_tag_performance(self, update: Update, context: CallbackContext) -> None: """ Handler for /mix_tags. Shows a performance statistic from finished trades @@ -1264,17 +1372,17 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_mix_tag_performance", - query=update.callback_query) + await self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_mix_tag_performance", + query=update.callback_query) @authorized_only - def _count(self, update: Update, context: CallbackContext) -> None: + async def _count(self, update: Update, context: CallbackContext) -> None: """ Handler for /count. Returns the number of trades running @@ -1286,21 +1394,21 @@ class Telegram(RPCHandler): message = tabulate({k: [v] for k, v in counts.items()}, headers=['current', 'max', 'total stake'], tablefmt='simple') - message = "
{}
".format(message) + message = f"
{message}
" logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML, - reload_able=True, callback_path="update_count", - query=update.callback_query) + await self._send_msg(message, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_count", + query=update.callback_query) @authorized_only - def _locks(self, update: Update, context: CallbackContext) -> None: + async def _locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /locks. Returns the currently active locks """ rpc_locks = self._rpc._rpc_locks() if not rpc_locks['locks']: - self._send_msg('No active locks.', parse_mode=ParseMode.HTML) + await self._send_msg('No active locks.', parse_mode=ParseMode.HTML) for locks in chunks(rpc_locks['locks'], 25): message = tabulate([[ @@ -1312,10 +1420,10 @@ class Telegram(RPCHandler): tablefmt='simple') message = f"
{escape(message)}
" logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _delete_locks(self, update: Update, context: CallbackContext) -> None: + async def _delete_locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete_locks. Returns the currently active locks @@ -1330,10 +1438,10 @@ class Telegram(RPCHandler): pair = arg self._rpc._rpc_delete_lock(lockid=lockid, pair=pair) - self._locks(update, context) + await self._locks(update, context) @authorized_only - def _whitelist(self, update: Update, context: CallbackContext) -> None: + async def _whitelist(self, update: Update, context: CallbackContext) -> None: """ Handler for /whitelist Shows the currently active whitelist @@ -1350,39 +1458,39 @@ class Telegram(RPCHandler): message += f"`{', '.join(whitelist['whitelist'])}`" logger.debug(message) - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _blacklist(self, update: Update, context: CallbackContext) -> None: + async def _blacklist(self, update: Update, context: CallbackContext) -> None: """ Handler for /blacklist Shows the currently active blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) + await self.send_blacklist_msg(self._rpc._rpc_blacklist(context.args)) - def send_blacklist_msg(self, blacklist: Dict): + async def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") + errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: - self._send_msg('\n'.join(errmsgs)) + await self._send_msg('\n'.join(errmsgs)) message = f"Blacklist contains {blacklist['length']} pairs\n" message += f"`{', '.join(blacklist['blacklist'])}`" logger.debug(message) - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: + async def _blacklist_delete(self, update: Update, context: CallbackContext) -> None: """ Handler for /bl_delete Deletes pair(s) from current blacklist """ - self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) + await self.send_blacklist_msg(self._rpc._rpc_blacklist_delete(context.args or [])) @authorized_only - def _logs(self, update: Update, context: CallbackContext) -> None: + async def _logs(self, update: Update, context: CallbackContext) -> None: """ Handler for /logs Shows the latest logs @@ -1401,17 +1509,17 @@ class Telegram(RPCHandler): escape_markdown(logrec[4], version=2)) if len(msgs + msg) + 10 >= MAX_MESSAGE_LENGTH: # Send message immediately if it would become too long - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) msgs = msg + '\n' else: # Append message to messages to send msgs += msg + '\n' if msgs: - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) @authorized_only - def _edge(self, update: Update, context: CallbackContext) -> None: + async def _edge(self, update: Update, context: CallbackContext) -> None: """ Handler for /edge Shows information related to Edge @@ -1419,17 +1527,17 @@ class Telegram(RPCHandler): edge_pairs = self._rpc._rpc_edge() if not edge_pairs: message = 'Edge only validated following pairs:' - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) for chunk in chunks(edge_pairs, 25): edge_pairs_tab = tabulate(chunk, headers='keys', tablefmt='simple') message = (f'Edge only validated following pairs:\n' f'
{edge_pairs_tab}
') - self._send_msg(message, parse_mode=ParseMode.HTML) + await self._send_msg(message, parse_mode=ParseMode.HTML) @authorized_only - def _help(self, update: Update, context: CallbackContext) -> None: + async def _help(self, update: Update, context: CallbackContext) -> None: """ Handler for /help. Show commands of the bot @@ -1456,6 +1564,11 @@ class Telegram(RPCHandler): "*/fx |all:* `Alias to /forceexit`\n" f"{force_enter_text if self._config.get('force_entry_enable', False) else ''}" "*/delete :* `Instantly delete the given trade in the database`\n" + "*/reload_trade :* `Relade trade from exchange Orders`\n" + "*/cancel_open_order :* `Cancels open orders for trade. " + "Only valid when the trade has open orders.`\n" + "*/coo |all:* `Alias to /cancel_open_order`\n" + "*/whitelist [sorted] [baseonly]:* `Show current whitelist. Optionally in " "order and/or only displaying the base currency of each pairing.`\n" "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " @@ -1469,11 +1582,15 @@ class Telegram(RPCHandler): "------------\n" "*/show_config:* `Show running configuration` \n" "*/locks:* `Show currently locked pairs`\n" - "*/balance:* `Show account balance per currency`\n" + "*/balance:* `Show bot managed balance per currency`\n" + "*/balance total:* `Show account balance per currency`\n" "*/logs [limit]:* `Show latest logs - defaults to 10` \n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" "*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n" + "*/marketdir [long | short | even | none]:* `Updates the user managed variable " + "that represents the current market direction. If no direction is provided `" + "`the currently set market direction will be output.` \n" "_Statistics_\n" "------------\n" @@ -1499,20 +1616,20 @@ class Telegram(RPCHandler): "*/version:* `Show version`" ) - self._send_msg(message, parse_mode=ParseMode.MARKDOWN) + await self._send_msg(message, parse_mode=ParseMode.MARKDOWN) @authorized_only - def _health(self, update: Update, context: CallbackContext) -> None: + async def _health(self, update: Update, context: CallbackContext) -> None: """ Handler for /health Shows the last process timestamp """ - health = self._rpc._health() + health = self._rpc.health() message = f"Last process: `{health['last_process_loc']}`" - self._send_msg(message) + await self._send_msg(message) @authorized_only - def _version(self, update: Update, context: CallbackContext) -> None: + async def _version(self, update: Update, context: CallbackContext) -> None: """ Handler for /version. Show version information @@ -1523,12 +1640,12 @@ class Telegram(RPCHandler): strategy_version = self._rpc._freqtrade.strategy.version() version_string = f'*Version:* `{__version__}`' if strategy_version is not None: - version_string += f', *Strategy version: * `{strategy_version}`' + version_string += f'\n*Strategy version: * `{strategy_version}`' - self._send_msg(version_string) + await self._send_msg(version_string) @authorized_only - def _show_config(self, update: Update, context: CallbackContext) -> None: + async def _show_config(self, update: Update, context: CallbackContext) -> None: """ Handler for /show_config. Show config information information @@ -1557,7 +1674,7 @@ class Telegram(RPCHandler): else: pa_info = "*Position adjustment:* Off\n" - self._send_msg( + await self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Market: * `{val['trading_mode']}`\n" @@ -1573,22 +1690,22 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) - def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", - reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: + async def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", + reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: if reload_able: reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("Refresh", callback_data=callback_path)], ]) else: reply_markup = InlineKeyboardMarkup([[]]) - msg += "\nUpdated: {}".format(datetime.now().ctime()) + msg += f"\nUpdated: {datetime.now().ctime()}" if not query.message: return chat_id = query.message.chat_id message_id = query.message.message_id try: - self._updater.bot.edit_message_text( + await self._app.bot.edit_message_text( chat_id=chat_id, message_id=message_id, text=msg, @@ -1603,12 +1720,12 @@ class Telegram(RPCHandler): except TelegramError as telegram_err: logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message) - def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, - disable_notification: bool = False, - keyboard: List[List[InlineKeyboardButton]] = None, - callback_path: str = "", - reload_able: bool = False, - query: Optional[CallbackQuery] = None) -> None: + async def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, + disable_notification: bool = False, + keyboard: Optional[List[List[InlineKeyboardButton]]] = None, + callback_path: str = "", + reload_able: bool = False, + query: Optional[CallbackQuery] = None) -> None: """ Send given markdown message :param msg: message @@ -1618,20 +1735,20 @@ class Telegram(RPCHandler): """ reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] if query: - self._update_msg(query=query, msg=msg, parse_mode=parse_mode, - callback_path=callback_path, reload_able=reload_able) + await self._update_msg(query=query, msg=msg, parse_mode=parse_mode, + callback_path=callback_path, reload_able=reload_able) return if reload_able and self._config['telegram'].get('reload', True): reply_markup = InlineKeyboardMarkup([ [InlineKeyboardButton("Refresh", callback_data=callback_path)]]) else: if keyboard is not None: - reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True) + reply_markup = InlineKeyboardMarkup(keyboard) else: reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) try: try: - self._updater.bot.send_message( + await self._app.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, @@ -1645,7 +1762,7 @@ class Telegram(RPCHandler): 'Telegram NetworkError: %s! Trying one more time.', network_err.message ) - self._updater.bot.send_message( + await self._app.bot.send_message( self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, @@ -1657,3 +1774,39 @@ class Telegram(RPCHandler): 'TelegramError: %s! Giving up on that message.', telegram_err.message ) + + @authorized_only + async def _changemarketdir(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /marketdir. + Updates the bot's market_direction + :param bot: telegram bot + :param update: message update + :return: None + """ + if context.args and len(context.args) == 1: + new_market_dir_arg = context.args[0] + old_market_dir = self._rpc._get_market_direction() + new_market_dir = None + if new_market_dir_arg == "long": + new_market_dir = MarketDirection.LONG + elif new_market_dir_arg == "short": + new_market_dir = MarketDirection.SHORT + elif new_market_dir_arg == "even": + new_market_dir = MarketDirection.EVEN + elif new_market_dir_arg == "none": + new_market_dir = MarketDirection.NONE + + if new_market_dir is not None: + self._rpc._update_market_direction(new_market_dir) + await self._send_msg("Successfully updated market direction" + f" from *{old_market_dir}* to *{new_market_dir}*.") + else: + raise RPCException("Invalid market direction provided. \n" + "Valid market directions: *long, short, even, none*") + elif context.args is not None and len(context.args) == 0: + old_market_dir = self._rpc._get_market_direction() + await self._send_msg(f"Currently set market direction: *{old_market_dir}*") + else: + raise RPCException("Invalid usage of command /marketdir. \n" + "Usage: */marketdir [short | long | even | none]*") diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index d81d8d24f..80690ec0c 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -10,6 +10,7 @@ from requests import RequestException, post from freqtrade.constants import Config from freqtrade.enums import RPCMessageType from freqtrade.rpc import RPC, RPCHandler +from freqtrade.rpc.rpc_types import RPCSendMsg logger = logging.getLogger(__name__) @@ -41,10 +42,13 @@ class Webhook(RPCHandler): """ pass - def _get_value_dict(self, msg: Dict[str, Any]) -> Optional[Dict[str, Any]]: + def _get_value_dict(self, msg: RPCSendMsg) -> Optional[Dict[str, Any]]: whconfig = self._config['webhook'] + if msg['type'].value in whconfig: + # Explicit types should have priority + valuedict = whconfig.get(msg['type'].value) # Deprecated 2022.10 - only keep generic method. - if msg['type'] in [RPCMessageType.ENTRY]: + elif msg['type'] in [RPCMessageType.ENTRY]: valuedict = whconfig.get('webhookentry') elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]: valuedict = whconfig.get('webhookentrycancel') @@ -58,11 +62,9 @@ class Webhook(RPCHandler): valuedict = whconfig.get('webhookexitcancel') elif msg['type'] in (RPCMessageType.STATUS, RPCMessageType.STARTUP, + RPCMessageType.EXCEPTION, RPCMessageType.WARNING): valuedict = whconfig.get('webhookstatus') - elif msg['type'].value in whconfig: - # Allow all types ... - valuedict = whconfig.get(msg['type'].value) elif msg['type'] in ( RPCMessageType.PROTECTION_TRIGGER, RPCMessageType.PROTECTION_TRIGGER_GLOBAL, @@ -74,7 +76,7 @@ class Webhook(RPCHandler): return None return valuedict - def send_msg(self, msg: Dict[str, Any]) -> None: + def send_msg(self, msg: RPCSendMsg) -> None: """ Send a message to telegram channel """ try: @@ -112,7 +114,7 @@ class Webhook(RPCHandler): response = post(self._url, data=payload['data'], headers={'Content-Type': 'text/plain'}) else: - raise NotImplementedError('Unknown format: {}'.format(self._format)) + raise NotImplementedError(f'Unknown format: {self._format}') # Throw a RequestException if the post was not successful response.raise_for_status() diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 6f62c9d3d..d38110a2a 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -4,11 +4,11 @@ This module defines a base class for auto-hyperoptable strategies. """ import logging from pathlib import Path -from typing import Any, Dict, Iterator, List, Tuple, Type, Union +from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union from freqtrade.constants import Config from freqtrade.exceptions import OperationalException -from freqtrade.misc import deep_merge_dicts, json_load +from freqtrade.misc import deep_merge_dicts from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.strategy.parameters import BaseParameter @@ -36,7 +36,8 @@ class HyperStrategyMixin: self._ft_params_from_file = params # Init/loading of parameters is done as part of ft_bot_start(). - def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]: + def enumerate_parameters( + self, category: Optional[str] = None) -> Iterator[Tuple[str, BaseParameter]]: """ Find all optimizable parameters and return (name, attr) iterator. :param category: @@ -80,6 +81,8 @@ class HyperStrategyMixin: self.stoploss = params.get('stoploss', {}).get( 'stoploss', getattr(self, 'stoploss', -0.1)) + self.max_open_trades = params.get('max_open_trades', {}).get( + 'max_open_trades', getattr(self, 'max_open_trades', -1)) trailing = params.get('trailing', {}) self.trailing_stop = trailing.get( 'trailing_stop', getattr(self, 'trailing_stop', False)) @@ -121,8 +124,7 @@ class HyperStrategyMixin: if filename.is_file(): logger.info(f"Loading parameters from file {filename}") try: - with filename.open('r') as f: - params = json_load(f) + params = HyperoptTools.load_params(filename) if params.get('strategy_name') != self.__class__.__name__: raise OperationalException('Invalid parameter file provided.') return params @@ -160,7 +162,7 @@ class HyperStrategyMixin: else: logger.info(f'Strategy Parameter(default): {attr_name} = {attr.value}') - def get_no_optimize_params(self): + def get_no_optimize_params(self) -> Dict[str, Dict]: """ Returns list of Parameters that are not part of the current optimize job """ @@ -170,7 +172,7 @@ class HyperStrategyMixin: 'protection': {}, } for name, p in self.enumerate_parameters(): - if not p.optimize or not p.in_space: + if p.category and (not p.optimize or not p.in_space): params[p.category][name] = p.value return params diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 50ae2341e..382f38c9a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,13 +7,12 @@ from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional, Tuple, Union -import arrow from pandas import DataFrame -from freqtrade.constants import Config, ListPairsWithTimeframes +from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, Config, IntOrInf, ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RunMode, SignalDirection, - SignalTagType, SignalType, TradingMode) +from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, RunMode, + SignalDirection, SignalTagType, SignalType, TradingMode) from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds from freqtrade.misc import remove_entry_exit_signals @@ -23,11 +22,11 @@ from freqtrade.strategy.informative_decorator import (InformativeData, PopulateI _create_and_merge_informative_pair, _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.util import dt_now from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) -CUSTOM_EXIT_MAX_LENGTH = 64 class IStrategy(ABC, HyperStrategyMixin): @@ -54,6 +53,9 @@ class IStrategy(ABC, HyperStrategyMixin): # associated stoploss stoploss: float + # max open trades for the strategy + max_open_trades: IntOrInf + # trailing stoploss trailing_stop: bool = False trailing_stop_positive: Optional[float] = None @@ -119,6 +121,9 @@ class IStrategy(ABC, HyperStrategyMixin): # Definition of plot_config. See plotting documentation for more details. plot_config: Dict = {} + # A self set parameter that represents the market direction. filled from configuration + market_direction: MarketDirection = MarketDirection.NONE + def __init__(self, config: Config) -> None: self.config = config # Dict to determine if analysis is necessary @@ -245,11 +250,12 @@ class IStrategy(ABC, HyperStrategyMixin): """ pass - def bot_loop_start(self, **kwargs) -> None: + def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks (e.g. gather some remote resource for comparison) + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass @@ -595,7 +601,7 @@ class IStrategy(ABC, HyperStrategyMixin): return None def populate_any_indicators(self, pair: str, df: DataFrame, tf: str, - informative: DataFrame = None, + informative: Optional[DataFrame] = None, set_generalized_indicators: bool = False) -> DataFrame: """ DEPRECATED - USE FEATURE ENGINEERING FUNCTIONS INSTEAD @@ -611,8 +617,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return df - def feature_engineering_expand_all(self, dataframe: DataFrame, - period: int, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -631,13 +637,15 @@ class IStrategy(ABC, HyperStrategyMixin): https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features :param period: period of the indicator - usage example: + :param metadata: metadata of current pair dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) """ return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -659,13 +667,15 @@ class IStrategy(ABC, HyperStrategyMixin): https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features + :param metadata: metadata of current pair dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) """ return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -683,12 +693,13 @@ class IStrategy(ABC, HyperStrategyMixin): https://www.freqtrade.io/en/latest/freqai-feature-engineering - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features + :param metadata: metadata of current pair usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 """ return dataframe - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -698,7 +709,8 @@ class IStrategy(ABC, HyperStrategyMixin): https://www.freqtrade.io/en/latest/freqai-feature-engineering - :param df: strategy dataframe which will receive the targets + :param dataframe: strategy dataframe which will receive the targets + :param metadata: metadata of current pair usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] """ return dataframe @@ -756,7 +768,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return self.__class__.__name__ - def lock_pair(self, pair: str, until: datetime, reason: str = None, side: str = '*') -> None: + def lock_pair(self, pair: str, until: datetime, + reason: Optional[str] = None, side: str = '*') -> None: """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. @@ -788,7 +801,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ PairLocks.unlock_reason(reason, datetime.now(timezone.utc)) - def is_pair_locked(self, pair: str, *, candle_date: datetime = None, side: str = '*') -> bool: + def is_pair_locked(self, pair: str, *, candle_date: Optional[datetime] = None, + side: str = '*') -> bool: """ Checks if a pair is currently locked The 2nd, optional parameter ensures that locks are applied until the new candle arrives, @@ -924,7 +938,7 @@ class IStrategy(ABC, HyperStrategyMixin): pair: str, timeframe: str, dataframe: DataFrame, - ) -> Tuple[Optional[DataFrame], Optional[arrow.Arrow]]: + ) -> Tuple[Optional[DataFrame], Optional[datetime]]: """ Calculates current signal based based on the entry order or exit order columns of the dataframe. @@ -940,16 +954,16 @@ class IStrategy(ABC, HyperStrategyMixin): latest_date = dataframe['date'].max() latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] - # Explicitly convert to arrow object to ensure the below comparison does not fail - latest_date = arrow.get(latest_date) + # Explicitly convert to datetime object to ensure the below comparison does not fail + latest_date = latest_date.to_pydatetime() # Check if dataframe is out of date timeframe_minutes = timeframe_to_minutes(timeframe) offset = self.config.get('exchange', {}).get('outdated_offset', 5) - if latest_date < (arrow.utcnow().shift(minutes=-(timeframe_minutes * 2 + offset))): + if latest_date < (dt_now() - timedelta(minutes=timeframe_minutes * 2 + offset)): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', - pair, int((arrow.utcnow() - latest_date).total_seconds() // 60) + pair, int((dt_now() - latest_date).total_seconds() // 60) ) return None, None return latest, latest_date @@ -959,7 +973,7 @@ class IStrategy(ABC, HyperStrategyMixin): pair: str, timeframe: str, dataframe: DataFrame, - is_short: bool = None + is_short: Optional[bool] = None ) -> Tuple[bool, bool, Optional[str]]: """ Calculates current exit signal based based on the dataframe @@ -1032,8 +1046,8 @@ class IStrategy(ABC, HyperStrategyMixin): timeframe_seconds = timeframe_to_seconds(timeframe) if self.ignore_expired_candle( - latest_date=latest_date.datetime, - current_time=datetime.now(timezone.utc), + latest_date=latest_date, + current_time=dt_now(), timeframe_seconds=timeframe_seconds, enter=bool(enter_signal) ): @@ -1058,7 +1072,7 @@ class IStrategy(ABC, HyperStrategyMixin): def should_exit(self, trade: Trade, rate: float, current_time: datetime, *, enter: bool, exit_: bool, - low: float = None, high: float = None, + low: Optional[float] = None, high: Optional[float] = None, force_stoploss: float = 0) -> List[ExitCheckTuple]: """ This function evaluates if one of the conditions required to trigger an exit order @@ -1074,10 +1088,10 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_min_max_rates(high or current_rate, low or current_rate) - stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, - current_time=current_time, - current_profit=current_profit, - force_stoploss=force_stoploss, low=low, high=high) + stoplossflag = self.ft_stoploss_reached(current_rate=current_rate, trade=trade, + current_time=current_time, + current_profit=current_profit, + force_stoploss=force_stoploss, low=low, high=high) # Set current rate to high for backtesting exits current_rate = (low if trade.is_short else high) or rate @@ -1105,11 +1119,11 @@ class IStrategy(ABC, HyperStrategyMixin): exit_signal = ExitType.CUSTOM_EXIT if isinstance(reason_cust, str): custom_reason = reason_cust - if len(reason_cust) > CUSTOM_EXIT_MAX_LENGTH: + if len(reason_cust) > CUSTOM_TAG_MAX_LENGTH: logger.warning(f'Custom exit reason returned from ' f'custom_exit is too long and was trimmed' - f'to {CUSTOM_EXIT_MAX_LENGTH} characters.') - custom_reason = reason_cust[:CUSTOM_EXIT_MAX_LENGTH] + f'to {CUSTOM_TAG_MAX_LENGTH} characters.') + custom_reason = reason_cust[:CUSTOM_TAG_MAX_LENGTH] else: custom_reason = '' if ( @@ -1144,13 +1158,12 @@ class IStrategy(ABC, HyperStrategyMixin): return exits - def stop_loss_reached(self, current_rate: float, trade: Trade, - current_time: datetime, current_profit: float, - force_stoploss: float, low: float = None, - high: float = None) -> ExitCheckTuple: + def ft_stoploss_adjust(self, current_rate: float, trade: Trade, + current_time: datetime, current_profit: float, + force_stoploss: float, low: Optional[float] = None, + high: Optional[float] = None) -> None: """ - Based on current profit of the trade and configured (trailing) stoploss, - decides to exit or not + Adjust stop-loss dynamically if configured to do so. :param current_profit: current profit as ratio :param low: Low value of this candle, only set in backtesting :param high: High value of this candle, only set in backtesting @@ -1196,6 +1209,20 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_stop_loss(bound or current_rate, stop_loss_value) + def ft_stoploss_reached(self, current_rate: float, trade: Trade, + current_time: datetime, current_profit: float, + force_stoploss: float, low: Optional[float] = None, + high: Optional[float] = None) -> ExitCheckTuple: + """ + Based on current profit of the trade and configured (trailing) stoploss, + decides to exit or not + :param current_profit: current profit as ratio + :param low: Low value of this candle, only set in backtesting + :param high: High value of this candle, only set in backtesting + """ + self.ft_stoploss_adjust(current_rate, trade, current_time, current_profit, + force_stoploss, low, high) + sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short) sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short) liq_higher_long = (trade.liquidation_price diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index aa753a829..27ebe7e69 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -86,37 +86,41 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, def stoploss_from_open( open_relative_stop: float, current_profit: float, - is_short: bool = False + is_short: bool = False, + leverage: float = 1.0 ) -> float: """ - - Given the current profit, and a desired stop loss value relative to the open price, + Given the current profit, and a desired stop loss value relative to the trade entry price, return a stop loss value that is relative to the current price, and which can be returned from `custom_stoploss`. The requested stop can be positive for a stop above the open price, or negative for a stop below the open price. The return value is always >= 0. + `open_relative_stop` will be considered as adjusted for leverage if leverage is provided.. Returns 0 if the resulting stop price would be above/below (longs/shorts) the current price - :param open_relative_stop: Desired stop loss percentage relative to open price + :param open_relative_stop: Desired stop loss percentage, relative to the open price, + adjusted for leverage :param current_profit: The current profit percentage :param is_short: When true, perform the calculation for short instead of long + :param leverage: Leverage to use for the calculation :return: Stop loss value relative to current price """ # formula is undefined for current_profit -1 (longs) or 1 (shorts), return maximum value - if (current_profit == -1 and not is_short) or (is_short and current_profit == 1): + _current_profit = current_profit / leverage + if (_current_profit == -1 and not is_short) or (is_short and _current_profit == 1): return 1 if is_short is True: - stoploss = -1 + ((1 - open_relative_stop) / (1 - current_profit)) + stoploss = -1 + ((1 - open_relative_stop / leverage) / (1 - _current_profit)) else: - stoploss = 1 - ((1 + open_relative_stop) / (1 + current_profit)) + stoploss = 1 - ((1 + open_relative_stop / leverage) / (1 + _current_profit)) # negative stoploss values indicate the requested stop price is higher/lower # (long/short) than the current price - return max(stoploss, 0.0) + return max(stoploss * leverage, 0.0) def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool = False) -> float: diff --git a/freqtrade/strategy/strategyupdater.py b/freqtrade/strategy/strategyupdater.py new file mode 100644 index 000000000..2669dcc4a --- /dev/null +++ b/freqtrade/strategy/strategyupdater.py @@ -0,0 +1,255 @@ +import shutil +from pathlib import Path + +import ast_comments + +from freqtrade.constants import Config + + +class StrategyUpdater: + name_mapping = { + 'ticker_interval': 'timeframe', + 'buy': 'enter_long', + 'sell': 'exit_long', + 'buy_tag': 'enter_tag', + 'sell_reason': 'exit_reason', + + 'sell_signal': 'exit_signal', + 'custom_sell': 'custom_exit', + 'force_sell': 'force_exit', + 'emergency_sell': 'emergency_exit', + + # Strategy/config settings: + 'use_sell_signal': 'use_exit_signal', + 'sell_profit_only': 'exit_profit_only', + 'sell_profit_offset': 'exit_profit_offset', + 'ignore_roi_if_buy_signal': 'ignore_roi_if_entry_signal', + 'forcebuy_enable': 'force_entry_enable', + } + + function_mapping = { + 'populate_buy_trend': 'populate_entry_trend', + 'populate_sell_trend': 'populate_exit_trend', + 'custom_sell': 'custom_exit', + 'check_buy_timeout': 'check_entry_timeout', + 'check_sell_timeout': 'check_exit_timeout', + # '': '', + } + # order_time_in_force, order_types, unfilledtimeout + otif_ot_unfilledtimeout = { + 'buy': 'entry', + 'sell': 'exit', + } + + # create a dictionary that maps the old column names to the new ones + rename_dict = {'buy': 'enter_long', 'sell': 'exit_long', 'buy_tag': 'enter_tag'} + + def start(self, config: Config, strategy_obj: dict) -> None: + """ + Run strategy updater + It updates a strategy to v3 with the help of the ast-module + :return: None + """ + + source_file = strategy_obj['location'] + strategies_backup_folder = Path.joinpath(config['user_data_dir'], "strategies_orig_updater") + target_file = Path.joinpath(strategies_backup_folder, strategy_obj['location_rel']) + + # read the file + with Path(source_file).open('r') as f: + old_code = f.read() + if not strategies_backup_folder.is_dir(): + Path(strategies_backup_folder).mkdir(parents=True, exist_ok=True) + + # backup original + # => currently no date after the filename, + # could get overridden pretty fast if this is fired twice! + # The folder is always the same and the file name too (currently). + shutil.copy(source_file, target_file) + + # update the code + new_code = self.update_code(old_code) + # write the modified code to the destination folder + with Path(source_file).open('w') as f: + f.write(new_code) + + # define the function to update the code + def update_code(self, code): + # parse the code into an AST + tree = ast_comments.parse(code) + + # use the AST to update the code + updated_code = self.modify_ast(tree) + + # return the modified code without executing it + return updated_code + + # function that uses the ast module to update the code + def modify_ast(self, tree): # noqa + # use the visitor to update the names and functions in the AST + NameUpdater().visit(tree) + + # first fix the comments, so it understands "\n" properly inside multi line comments. + ast_comments.fix_missing_locations(tree) + ast_comments.increment_lineno(tree, n=1) + + # generate the new code from the updated AST + # without indent {} parameters would just be written straight one after the other. + + # ast_comments would be amazing since this is the only solution that carries over comments, + # but it does currently not have an unparse function, hopefully in the future ... ! + # return ast_comments.unparse(tree) + + return ast_comments.unparse(tree) + + +# Here we go through each respective node, slice, elt, key ... to replace outdated entries. +class NameUpdater(ast_comments.NodeTransformer): + def generic_visit(self, node): + + # space is not yet transferred from buy/sell to entry/exit and thereby has to be skipped. + if isinstance(node, ast_comments.keyword): + if node.arg == "space": + return node + + # from here on this is the original function. + for field, old_value in ast_comments.iter_fields(node): + if isinstance(old_value, list): + new_values = [] + for value in old_value: + if isinstance(value, ast_comments.AST): + value = self.visit(value) + if value is None: + continue + elif not isinstance(value, ast_comments.AST): + new_values.extend(value) + continue + new_values.append(value) + old_value[:] = new_values + elif isinstance(old_value, ast_comments.AST): + new_node = self.visit(old_value) + if new_node is None: + delattr(node, field) + else: + setattr(node, field, new_node) + return node + + def visit_Expr(self, node): + if hasattr(node.value, "left") and hasattr(node.value.left, "id"): + node.value.left.id = self.check_dict(StrategyUpdater.name_mapping, node.value.left.id) + self.visit(node.value) + return node + + # Renames an element if contained inside a dictionary. + @staticmethod + def check_dict(current_dict: dict, element: str): + if element in current_dict: + element = current_dict[element] + return element + + def visit_arguments(self, node): + if isinstance(node.args, list): + for arg in node.args: + arg.arg = self.check_dict(StrategyUpdater.name_mapping, arg.arg) + return node + + def visit_Name(self, node): + # if the name is in the mapping, update it + node.id = self.check_dict(StrategyUpdater.name_mapping, node.id) + return node + + def visit_Import(self, node): + # do not update the names in import statements + return node + + def visit_ImportFrom(self, node): + # if hasattr(node, "module"): + # if node.module == "freqtrade.strategy.hyper": + # node.module = "freqtrade.strategy" + return node + + def visit_If(self, node: ast_comments.If): + for child in ast_comments.iter_child_nodes(node): + self.visit(child) + return node + + def visit_FunctionDef(self, node): + node.name = self.check_dict(StrategyUpdater.function_mapping, node.name) + self.generic_visit(node) + return node + + def visit_Attribute(self, node): + if ( + isinstance(node.value, ast_comments.Name) + and node.value.id == 'trade' + and node.attr == 'nr_of_successful_buys' + ): + node.attr = 'nr_of_successful_entries' + return node + + def visit_ClassDef(self, node): + # check if the class is derived from IStrategy + if any(isinstance(base, ast_comments.Name) and + base.id == 'IStrategy' for base in node.bases): + # check if the INTERFACE_VERSION variable exists + has_interface_version = any( + isinstance(child, ast_comments.Assign) and + isinstance(child.targets[0], ast_comments.Name) and + child.targets[0].id == 'INTERFACE_VERSION' + for child in node.body + ) + + # if the INTERFACE_VERSION variable does not exist, add it as the first child + if not has_interface_version: + node.body.insert(0, ast_comments.parse('INTERFACE_VERSION = 3').body[0]) + # otherwise, update its value to 3 + else: + for child in node.body: + if ( + isinstance(child, ast_comments.Assign) + and isinstance(child.targets[0], ast_comments.Name) + and child.targets[0].id == 'INTERFACE_VERSION' + ): + child.value = ast_comments.parse('3').body[0].value + self.generic_visit(node) + return node + + def visit_Subscript(self, node): + if isinstance(node.slice, ast_comments.Constant): + if node.slice.value in StrategyUpdater.rename_dict: + # Replace the slice attributes with the values from rename_dict + node.slice.value = StrategyUpdater.rename_dict[node.slice.value] + if hasattr(node.slice, "elts"): + self.visit_elts(node.slice.elts) + if hasattr(node.slice, "value"): + if hasattr(node.slice.value, "elts"): + self.visit_elts(node.slice.value.elts) + return node + + # elts can have elts (technically recursively) + def visit_elts(self, elts): + if isinstance(elts, list): + for elt in elts: + self.visit_elt(elt) + else: + self.visit_elt(elts) + return elts + + # sub function again needed since the structure itself is highly flexible ... + def visit_elt(self, elt): + if isinstance(elt, ast_comments.Constant) and elt.value in StrategyUpdater.rename_dict: + elt.value = StrategyUpdater.rename_dict[elt.value] + if hasattr(elt, "elts"): + self.visit_elts(elt.elts) + if hasattr(elt, "args"): + if isinstance(elt.args, ast_comments.arguments): + self.visit_elts(elt.args) + else: + for arg in elt.args: + self.visit_elts(arg) + return elt + + def visit_Constant(self, node): + node.value = self.check_dict(StrategyUpdater.otif_ot_unfilledtimeout, node.value) + node.value = self.check_dict(StrategyUpdater.name_mapping, node.value) + return node diff --git a/freqtrade/templates/FreqaiExampleHybridStrategy.py b/freqtrade/templates/FreqaiExampleHybridStrategy.py index c5dbe8dbd..03446d76e 100644 --- a/freqtrade/templates/FreqaiExampleHybridStrategy.py +++ b/freqtrade/templates/FreqaiExampleHybridStrategy.py @@ -1,12 +1,13 @@ import logging +from typing import Dict -import numpy as np -import pandas as pd +import numpy as np # noqa +import pandas as pd # noqa import talib.abstract as ta from pandas import DataFrame from technical import qtpylib -from freqtrade.strategy import IntParameter, IStrategy, merge_informative_pair +from freqtrade.strategy import IntParameter, IStrategy, merge_informative_pair # noqa logger = logging.getLogger(__name__) @@ -26,7 +27,7 @@ class FreqaiExampleHybridStrategy(IStrategy): "freqai": { "enabled": true, - "purge_old_models": true, + "purge_old_models": 2, "train_period_days": 15, "identifier": "uniqe-id", "feature_parameters": { @@ -95,7 +96,8 @@ class FreqaiExampleHybridStrategy(IStrategy): short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True) exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -114,8 +116,9 @@ class FreqaiExampleHybridStrategy(IStrategy): https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features :param period: period of the indicator - usage example: + :param metadata: metadata of current pair dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) """ @@ -148,7 +151,8 @@ class FreqaiExampleHybridStrategy(IStrategy): return dataframe - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -170,7 +174,8 @@ class FreqaiExampleHybridStrategy(IStrategy): https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features + :param metadata: metadata of current pair dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) """ @@ -179,7 +184,8 @@ class FreqaiExampleHybridStrategy(IStrategy): dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -197,14 +203,15 @@ class FreqaiExampleHybridStrategy(IStrategy): https://www.freqtrade.io/en/latest/freqai-feature-engineering - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features + :param metadata: metadata of current pair usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 """ dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -214,16 +221,17 @@ class FreqaiExampleHybridStrategy(IStrategy): https://www.freqtrade.io/en/latest/freqai-feature-engineering - :param df: strategy dataframe which will receive the targets + :param dataframe: strategy dataframe which will receive the targets + :param metadata: metadata of current pair usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] """ + self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) > - dataframe["close"], 'up', 'down') + dataframe["close"], 'up', 'down') return dataframe - # flake8: noqa: C901 - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # noqa: C901 # User creates their own custom strat here. Present example is a supertrend # based strategy. diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 8e34d733e..347efdda0 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -1,5 +1,6 @@ import logging from functools import reduce +from typing import Dict import talib.abstract as ta from pandas import DataFrame @@ -14,12 +15,15 @@ logger = logging.getLogger(__name__) class FreqaiExampleStrategy(IStrategy): """ Example strategy showing how the user connects their own - IFreqaiModel to the strategy. Namely, the user uses: - self.freqai.start(dataframe, metadata) + IFreqaiModel to the strategy. - to make predictions on their data. feature_engineering_*() automatically - generate the variety of features indicated by the user in the - canonical freqtrade configuration file under config['freqai']. + Warning! This is a showcase of functionality, + which means that it is designed to show various functions of FreqAI + and it runs on all computers. We use this showcase to help users + understand how to build a strategy, and we use it as a benchmark + to help debug possible problems. + + This means this is *not* meant to be run live in production. """ minimal_roi = {"0": 0.1, "240": -1} @@ -46,7 +50,8 @@ class FreqaiExampleStrategy(IStrategy): std_dev_multiplier_sell = CategoricalParameter( [0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True) - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -58,6 +63,10 @@ class FreqaiExampleStrategy(IStrategy): All features must be prepended with `%` to be recognized by FreqAI internals. + Access metadata such as the current pair/timeframe with: + + `metadata["pair"]` `metadata["tf"]` + More details on how these config defined parameters accelerate feature engineering in the documentation at: @@ -65,8 +74,9 @@ class FreqaiExampleStrategy(IStrategy): https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features :param period: period of the indicator - usage example: + :param metadata: metadata of current pair dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) """ @@ -99,7 +109,8 @@ class FreqaiExampleStrategy(IStrategy): return dataframe - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -114,6 +125,10 @@ class FreqaiExampleStrategy(IStrategy): All features must be prepended with `%` to be recognized by FreqAI internals. + Access metadata such as the current pair/timeframe with: + + `metadata["pair"]` `metadata["tf"]` + More details on how these config defined parameters accelerate feature engineering in the documentation at: @@ -121,7 +136,8 @@ class FreqaiExampleStrategy(IStrategy): https://www.freqtrade.io/en/latest/freqai-feature-engineering/#defining-the-features - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features + :param metadata: metadata of current pair dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-ema-200"] = ta.EMA(dataframe, timeperiod=200) """ @@ -130,7 +146,8 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -144,28 +161,38 @@ class FreqaiExampleStrategy(IStrategy): All features must be prepended with `%` to be recognized by FreqAI internals. + Access metadata such as the current pair with: + + `metadata["pair"]` + More details about feature engineering available: https://www.freqtrade.io/en/latest/freqai-feature-engineering - :param df: strategy dataframe which will receive the features + :param dataframe: strategy dataframe which will receive the features + :param metadata: metadata of current pair usage example: dataframe["%-day_of_week"] = (dataframe["date"].dt.dayofweek + 1) / 7 """ dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals. + Access metadata such as the current pair with: + + `metadata["pair"]` + More details about feature engineering available: https://www.freqtrade.io/en/latest/freqai-feature-engineering - :param df: strategy dataframe which will receive the targets + :param dataframe: strategy dataframe which will receive the targets + :param metadata: metadata of current pair usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] """ dataframe["&-s_close"] = ( diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 299734a50..1a4552c11 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -41,20 +41,6 @@ "pairlists": [ {{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }} ], - "edge": { - "enabled": false, - "process_throttle_secs": 3600, - "calculate_since_number_of_days": 7, - "allowed_risk": 0.01, - "stoploss_range_min": -0.01, - "stoploss_range_max": -0.1, - "stoploss_range_step": -0.01, - "minimum_winrate": 0.60, - "minimum_expectancy": 0.20, - "min_trade_number": 10, - "max_trade_duration_minute": 1440, - "remove_pumps": false - }, "telegram": { "enabled": {{ telegram | lower }}, "token": "{{ telegram_token }}", diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index dfbcedb72..2bfa4155d 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -118,6 +118,7 @@ "from freqtrade.data.dataprovider import DataProvider\n", "strategy = StrategyResolver.load_strategy(config)\n", "strategy.dp = DataProvider(config, None, None)\n", + "strategy.ft_bot_start()\n", "\n", "# Generate buy/sell signals using strategy\n", "df = strategy.analyze_ticker(candles, {'pair': pair})\n", diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index 488ca2fd7..bfbb20ec1 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -1,5 +1,5 @@ -def bot_loop_start(self, **kwargs) -> None: +def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks @@ -8,6 +8,7 @@ def bot_loop_start(self, **kwargs) -> None: For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ When not implemented by a strategy, this simply does nothing. + :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 7980b7ca2..bed65a54b 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -1,3 +1,17 @@ -# flake8: noqa: F401 +from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, + dt_utc, shorten_date) from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.periodic_cache import PeriodicCache + + +__all__ = [ + 'dt_floor_day', + 'dt_from_ts', + 'dt_now', + 'dt_ts', + 'dt_utc', + 'dt_humanize', + 'shorten_date', + 'FtPrecise', + 'PeriodicCache', +] diff --git a/freqtrade/util/binance_mig.py b/freqtrade/util/binance_mig.py new file mode 100644 index 000000000..37a2d2ef1 --- /dev/null +++ b/freqtrade/util/binance_mig.py @@ -0,0 +1,79 @@ +import logging + +from packaging import version +from sqlalchemy import select + +from freqtrade.constants import Config +from freqtrade.enums.tradingmode import TradingMode +from freqtrade.exceptions import OperationalException +from freqtrade.persistence.pairlock import PairLock +from freqtrade.persistence.trade_model import Trade + + +logger = logging.getLogger(__name__) + + +def migrate_binance_futures_names(config: Config): + + if ( + not (config.get('trading_mode', TradingMode.SPOT) == TradingMode.FUTURES + and config['exchange']['name'] == 'binance') + ): + # only act on new futures + return + import ccxt + if version.parse("2.6.26") > version.parse(ccxt.__version__): + raise OperationalException( + "Please follow the update instructions in the docs " + "(https://www.freqtrade.io/en/latest/updating/) to install a compatible ccxt version.") + _migrate_binance_futures_db(config) + migrate_binance_futures_data(config) + + +def _migrate_binance_futures_db(config: Config): + logger.warning('Migrating binance futures pairs in database.') + trades = Trade.get_trades([Trade.exchange == 'binance', Trade.trading_mode == 'FUTURES']).all() + for trade in trades: + if ':' in trade.pair: + # already migrated + continue + new_pair = f"{trade.pair}:{trade.stake_currency}" + trade.pair = new_pair + + for order in trade.orders: + order.ft_pair = new_pair + # Should symbol be migrated too? + # order.symbol = new_pair + Trade.commit() + pls = PairLock.session.scalars(select(PairLock).filter(PairLock.pair.notlike('%:%'))).all() + for pl in pls: + pl.pair = f"{pl.pair}:{config['stake_currency']}" + # print(pls) + # pls.update({'pair': concat(PairLock.pair,':USDT')}) + Trade.commit() + logger.warning('Done migrating binance futures pairs in database.') + + +def migrate_binance_futures_data(config: Config): + + if ( + not (config.get('trading_mode', TradingMode.SPOT) == TradingMode.FUTURES + and config['exchange']['name'] == 'binance') + ): + # only act on new futures + return + + from freqtrade.data.history.idatahandler import get_datahandler + dhc = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', 'json')) + + paircombs = dhc.ohlcv_get_available_data( + config['datadir'], + config.get('trading_mode', TradingMode.SPOT) + ) + + for pair, timeframe, candle_type in paircombs: + if ':' in pair: + # already migrated + continue + new_pair = f"{pair}:{config['stake_currency']}" + dhc.rename_futures_data(pair, new_pair, timeframe, candle_type) diff --git a/freqtrade/util/datetime_helpers.py b/freqtrade/util/datetime_helpers.py new file mode 100644 index 000000000..39d134e11 --- /dev/null +++ b/freqtrade/util/datetime_helpers.py @@ -0,0 +1,63 @@ +import re +from datetime import datetime, timezone +from typing import Optional + +import arrow + + +def dt_now() -> datetime: + """Return the current datetime in UTC.""" + return datetime.now(timezone.utc) + + +def dt_utc(year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0, + microsecond: int = 0) -> datetime: + """Return a datetime in UTC.""" + return datetime(year, month, day, hour, minute, second, microsecond, tzinfo=timezone.utc) + + +def dt_ts(dt: Optional[datetime] = None) -> int: + """ + Return dt in ms as a timestamp in UTC. + If dt is None, return the current datetime in UTC. + """ + if dt: + return int(dt.timestamp() * 1000) + return int(dt_now().timestamp() * 1000) + + +def dt_floor_day(dt: datetime) -> datetime: + """Return the floor of the day for the given datetime.""" + return dt.replace(hour=0, minute=0, second=0, microsecond=0) + + +def dt_from_ts(timestamp: float) -> datetime: + """ + Return a datetime from a timestamp. + :param timestamp: timestamp in seconds or milliseconds + """ + if timestamp > 1e10: + # Timezone in ms - convert to seconds + timestamp /= 1000 + return datetime.fromtimestamp(timestamp, tz=timezone.utc) + + +def shorten_date(_date: str) -> str: + """ + Trim the date so it fits on small screens + """ + new_date = re.sub('seconds?', 'sec', _date) + new_date = re.sub('minutes?', 'min', new_date) + new_date = re.sub('hours?', 'h', new_date) + new_date = re.sub('days?', 'd', new_date) + new_date = re.sub('^an?', '1', new_date) + return new_date + + +def dt_humanize(dt: datetime, **kwargs) -> str: + """ + Return a humanized string for the given datetime. + :param dt: datetime to humanize + :param kwargs: kwargs to pass to arrow's humanize() + """ + return arrow.get(dt).humanize(**kwargs) diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index 4f14ae13c..63797d462 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# # QTPyLib: Quantitative Trading Python Library # https://github.com/ranaroussi/qtpylib # @@ -19,7 +16,6 @@ # limitations under the License. # -import sys import warnings from datetime import datetime, timedelta @@ -28,11 +24,6 @@ import pandas as pd from pandas.core.base import PandasObject -# ============================================= -# check min, python version -if sys.version_info < (3, 4): - raise SystemError("QTPyLib requires Python version >= 3.4") - # ============================================= warnings.simplefilter(action="ignore", category=RuntimeWarning) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 97db3fba5..da64515a4 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -3,15 +3,16 @@ import logging from copy import deepcopy +from datetime import datetime, timedelta from typing import Dict, NamedTuple, Optional -import arrow - from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.enums import RunMode, TradingMode from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange +from freqtrade.misc import safe_value_fallback from freqtrade.persistence import LocalTrade, Trade +from freqtrade.util.datetime_helpers import dt_now logger = logging.getLogger(__name__) @@ -42,7 +43,7 @@ class Wallets: self._wallets: Dict[str, Wallet] = {} self._positions: Dict[str, PositionWallet] = {} self.start_cap = config['dry_run_wallet'] - self._last_wallet_refresh = 0 + self._last_wallet_refresh: Optional[datetime] = None self.update() def get_free(self, currency: str) -> float: @@ -148,7 +149,7 @@ class Wallets: # Position is not open ... continue size = self._exchange._contracts_to_amount(symbol, position['contracts']) - collateral = position['collateral'] or 0.0 + collateral = safe_value_fallback(position, 'collateral', 'initialMargin', 0.0) leverage = position['leverage'] self._positions[symbol] = PositionWallet( symbol, position=size, @@ -165,14 +166,19 @@ class Wallets: for trading operations, the latest balance is needed. :param require_update: Allow skipping an update if balances were recently refreshed """ - if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)): + now = dt_now() + if ( + require_update + or self._last_wallet_refresh is None + or (self._last_wallet_refresh + timedelta(seconds=3600) < now) + ): if (not self._config['dry_run'] or self._config.get('runmode') == RunMode.LIVE): self._update_live() else: self._update_dry() if self._log: logger.info('Wallets synced.') - self._last_wallet_refresh = arrow.utcnow().int_timestamp + self._last_wallet_refresh = dt_now() def get_all_balances(self) -> Dict[str, Wallet]: return self._wallets @@ -180,6 +186,35 @@ class Wallets: def get_all_positions(self) -> Dict[str, PositionWallet]: return self._positions + def _check_exit_amount(self, trade: Trade) -> bool: + if trade.trading_mode != TradingMode.FUTURES: + # Slightly higher offset than in safe_exit_amount. + wallet_amount: float = self.get_total(trade.safe_base_currency) * (2 - 0.981) + else: + # wallet_amount: float = self.wallets.get_free(trade.safe_base_currency) + position = self._positions.get(trade.pair) + if position is None: + # We don't own anything :O + return False + wallet_amount = position.position + + if wallet_amount >= trade.amount: + return True + return False + + def check_exit_amount(self, trade: Trade) -> bool: + """ + Checks if the exit amount is available in the wallet. + :param trade: Trade to check + :return: True if the exit amount is available, False otherwise + """ + if not self._check_exit_amount(trade): + # Update wallets just to make sure + self.update() + return self._check_exit_amount(trade) + + return True + def get_starting_balance(self) -> float: """ Retrieves starting balance - based on either available capital, @@ -297,16 +332,16 @@ class Wallets: logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.") return 0 - max_stake_amount = min(max_stake_amount, self.get_available_stake_amount()) + max_allowed_stake = min(max_stake_amount, self.get_available_stake_amount()) if trade_amount: # if in a trade, then the resulting trade size cannot go beyond the max stake # Otherwise we could no longer exit. - max_stake_amount = min(max_stake_amount, max_stake_amount - trade_amount) + max_allowed_stake = min(max_allowed_stake, max_stake_amount - trade_amount) - if min_stake_amount is not None and min_stake_amount > max_stake_amount: + if min_stake_amount is not None and min_stake_amount > max_allowed_stake: if self._log: logger.warning("Minimum stake amount > available balance. " - f"{min_stake_amount} > {max_stake_amount}") + f"{min_stake_amount} > {max_allowed_stake}") return 0 if min_stake_amount is not None and stake_amount < min_stake_amount: if self._log: @@ -325,11 +360,11 @@ class Wallets: return 0 stake_amount = min_stake_amount - if stake_amount > max_stake_amount: + if stake_amount > max_allowed_stake: if self._log: logger.info( f"Stake amount for pair {pair} is too big " - f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}." + f"({stake_amount} > {max_allowed_stake}), adjusting to {max_allowed_stake}." ) - stake_amount = max_stake_amount + stake_amount = max_allowed_stake return stake_amount diff --git a/freqtrade/worker.py b/freqtrade/worker.py old mode 100755 new mode 100644 index 27f067b07..fb89e7a2d --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -12,7 +12,7 @@ import sdnotify from freqtrade import __version__ from freqtrade.configuration import Configuration from freqtrade.constants import PROCESS_THROTTLE_SECS, RETRY_TIMEOUT, Config -from freqtrade.enums import State +from freqtrade.enums import RPCMessageType, State from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.exchange import timeframe_to_next_date from freqtrade.freqtradebot import FreqtradeBot @@ -26,7 +26,7 @@ class Worker: Freqtradebot worker class """ - def __init__(self, args: Dict[str, Any], config: Config = None) -> None: + def __init__(self, args: Dict[str, Any], config: Optional[Config] = None) -> None: """ Init all variables and objects the bot needs to work """ @@ -185,7 +185,10 @@ class Worker: tb = traceback.format_exc() hint = 'Issue `/start` if you think it is safe to restart.' - self.freqtrade.notify_status(f'OperationalException:\n```\n{tb}```{hint}') + self.freqtrade.notify_status( + f'*OperationalException:*\n```\n{tb}```\n {hint}', + msg_type=RPCMessageType.EXCEPTION + ) logger.exception('OperationalException. Stopping trader ...') self.freqtrade.state = State.STOPPED diff --git a/pyproject.toml b/pyproject.toml index 2de2c957b..17f91c7b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools >= 64.0.0", "wheel"] +build-backend = "setuptools.build_meta" + [tool.black] line-length = 100 exclude = ''' @@ -31,19 +35,22 @@ asyncio_mode = "auto" [tool.mypy] ignore_missing_imports = true namespace_packages = false -implicit_optional = true warn_unused_ignores = true exclude = [ '^build_helpers\.py$' ] +plugins = [ + "sqlalchemy.ext.mypy.plugin" +] [[tool.mypy.overrides]] module = "tests.*" ignore_errors = true -[build-system] -requires = ["setuptools >= 46.4.0", "wheel"] -build-backend = "setuptools.build_meta" +[[tool.mypy.overrides]] +# Telegram does not use implicit_optional = false in the current version. +module = "telegram.*" +implicit_optional = true [tool.pyright] include = ["freqtrade"] @@ -53,5 +60,30 @@ exclude = [ ] ignore = ["freqtrade/vendor/**"] -# Align pyright to mypy config -strictParameterNoneValue = false + +[tool.ruff] +line-length = 100 +extend-exclude = [".env"] +target-version = "py38" +extend-select = [ + "C90", # mccabe + # "N", # pep8-naming + "F", # pyflakes + "E", # pycodestyle + "W", # pycodestyle + "UP", # pyupgrade + "TID", # flake8-tidy-imports + # "EXE", # flake8-executable + "YTT", # flake8-2020 + # "S", # flake8-bandit + # "DTZ", # flake8-datetimez + # "RSE", # flake8-raise + # "TCH", # flake8-type-checking + "PTH", # flake8-use-pathlib +] + +[tool.ruff.mccabe] +max-complexity = 12 + +[tool.ruff.per-file-ignores] +"tests/*" = ["S"] diff --git a/requirements-dev.txt b/requirements-dev.txt index f770d16be..cc3463174 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,27 +7,24 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -flake8==6.0.0 -flake8-tidy-imports==4.8.0 -mypy==0.991 -pre-commit==2.21.0 -pytest==7.2.0 -pytest-asyncio==0.20.3 +ruff==0.0.269 +mypy==1.3.0 +pre-commit==3.3.2 +pytest==7.3.1 +pytest-asyncio==0.21.0 pytest-cov==4.0.0 pytest-mock==3.10.0 pytest-random-order==1.1.0 -isort==5.11.4 +isort==5.12.0 # For datetime mocking time-machine==2.9.0 -# fastapi testing -httpx==0.23.3 # Convert jupyter notebooks to markdown documents -nbconvert==7.2.7 +nbconvert==7.4.0 # mypy types -types-cachetools==5.2.1 +types-cachetools==5.3.0.5 types-filelock==3.2.7 -types-requests==2.28.11.7 -types-tabulate==0.9.0.0 -types-python-dateutil==2.8.19.5 +types-requests==2.30.0.0 +types-tabulate==0.9.0.2 +types-python-dateutil==2.8.19.13 diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index db8d8d169..535d10f4b 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -2,8 +2,10 @@ -r requirements-freqai.txt # Required for freqai-rl -torch==1.13.1 -stable-baselines3==1.6.2 -sb3-contrib==1.6.2 -# Gym is forced to this version by stable-baselines3. -gym==0.21 +torch==2.0.1 +#until these branches will be released we can use this +gymnasium==0.28.1 +stable_baselines3==2.0.0a5 +sb3_contrib>=2.0.0a4 +# Progress bar for stable-baselines3 and sb3-contrib +tqdm==4.65.0 diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 478d619c0..ad069ade2 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -5,7 +5,8 @@ # Required for freqai scikit-learn==1.1.3 joblib==1.2.0 -catboost==1.1.1; platform_machine != 'aarch64' -lightgbm==3.3.4 -xgboost==1.7.2 -tensorboard==2.11.0 +catboost==1.1.1; sys_platform == 'darwin' and python_version < '3.9' +catboost==1.2; 'arm' not in platform_machine and (sys_platform != 'darwin' or python_version >= '3.9') +lightgbm==3.3.5 +xgboost==1.7.5 +tensorboard==2.13.0 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 171ede929..87b1fd3c8 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,8 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.10.0 +scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.9.0 -progressbar2==4.2.0 +filelock==3.12.0 diff --git a/requirements-plot.txt b/requirements-plot.txt index 75e3234a1..8b9ad5bc4 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,4 +1,4 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.11.0 +plotly==5.14.1 diff --git a/requirements.txt b/requirements.txt index 13571731e..48ecb4d34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,59 +1,64 @@ -numpy==1.24.1 -pandas==1.5.2 +numpy==1.24.3 +pandas==2.0.1 pandas-ta==0.3.14b -ccxt==2.5.56 -# Pin cryptography for now due to rust build errors with piwheels -cryptography==38.0.1; platform_machine == 'armv7l' -cryptography==38.0.4; platform_machine != 'armv7l' -aiohttp==3.8.3 -SQLAlchemy==1.4.46 -python-telegram-bot==13.15 +ccxt==3.1.5 +cryptography==40.0.2; platform_machine != 'armv7l' +cryptography==40.0.1; platform_machine == 'armv7l' +aiohttp==3.8.4 +SQLAlchemy==2.0.15 +python-telegram-bot==20.3 +# can't be hard-pinned due to telegram-bot pinning httpx with ~ +httpx>=0.23.3 arrow==1.2.3 -cachetools==4.2.2 -requests==2.28.1 -urllib3==1.26.13 +cachetools==5.3.0 +requests==2.30.0 +urllib3==2.0.2 jsonschema==4.17.3 -TA-Lib==0.4.25 -technical==1.3.0 +TA-Lib==0.4.26 +technical==1.4.0 tabulate==0.9.0 pycoingecko==3.1.0 jinja2==3.1.2 tables==3.8.0 blosc==1.11.1 joblib==1.2.0 -pyarrow==10.0.1; platform_machine != 'armv7l' +rich==13.3.5 +pyarrow==12.0.0; platform_machine != 'armv7l' # find first, C search in arrays 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.4 +orjson==3.8.12 # Notify systemd sdnotify==0.3.2 # API Server -fastapi==0.89.0 -pydantic==1.10.4 -uvicorn==0.20.0 -pyjwt==2.6.0 -aiofiles==22.1.0 -psutil==5.9.4 +fastapi==0.95.2 +pydantic==1.10.7 +uvicorn==0.22.0 +pyjwt==2.7.0 +aiofiles==23.1.0 +psutil==5.9.5 # Support for colorized terminal output colorama==0.4.6 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.36 +prompt-toolkit==3.0.38 # Extensions to datetime library python-dateutil==2.8.2 #Futures -schedule==1.1.0 +schedule==1.2.0 #WS Messages -websockets==10.4 +websockets==11.0.3 janus==1.0.0 + +ast-comments==1.0.1 +packaging==23.1 diff --git a/scripts/rest_client.py b/scripts/rest_client.py index ac6d97133..ccffe7f5f 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -14,6 +14,7 @@ import logging import re import sys from pathlib import Path +from typing import Optional from urllib.parse import urlencode, urlparse, urlunparse import rapidjson @@ -36,7 +37,7 @@ class FtRestClient(): self._session = requests.Session() self._session.auth = (username, password) - def _call(self, method, apipath, params: dict = None, data=None, files=None): + def _call(self, method, apipath, params: Optional[dict] = None, data=None, files=None): if str(method).upper() not in ('GET', 'POST', 'PUT', 'DELETE'): raise ValueError(f'invalid method <{method}>') @@ -60,13 +61,13 @@ class FtRestClient(): except ConnectionError: logger.warning("Connection error") - def _get(self, apipath, params: dict = None): + def _get(self, apipath, params: Optional[dict] = None): return self._call("GET", apipath, params=params) - def _delete(self, apipath, params: dict = None): + def _delete(self, apipath, params: Optional[dict] = None): return self._call("DELETE", apipath, params=params) - def _post(self, apipath, params: dict = None, data: dict = None): + def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None): return self._call("POST", apipath, params=params, data=data) def start(self): @@ -176,8 +177,7 @@ class FtRestClient(): return self._get("version") def show_config(self): - """ - Returns part of the configuration, relevant for trading operations. + """ Returns part of the configuration, relevant for trading operations. :return: json object containing the version """ return self._get("show_config") @@ -231,6 +231,14 @@ class FtRestClient(): """ return self._delete(f"trades/{trade_id}") + def cancel_open_order(self, trade_id): + """Cancel open order for trade. + + :param trade_id: Cancels open orders for this trade. + :return: json object + """ + return self._delete(f"trades/{trade_id}/open-order") + def whitelist(self): """Show the current whitelist. @@ -332,18 +340,21 @@ class FtRestClient(): :param limit: Limit result to the last n candles. :return: json object """ - return self._get("pair_candles", params={ + params = { "pair": pair, "timeframe": timeframe, - "limit": limit, - }) + } + if limit: + params['limit'] = limit + return self._get("pair_candles", params=params) - def pair_history(self, pair, timeframe, strategy, timerange=None): + def pair_history(self, pair, timeframe, strategy, timerange=None, freqaimodel=None): """Return historic, analyzed dataframe :param pair: Pair to get data for :param timeframe: Only pairs with this timeframe available. :param strategy: Strategy to analyze and get values for + :param freqaimodel: FreqAI model to use for analysis :param timerange: Timerange to get data for (same format than --timerange endpoints) :return: json object """ @@ -351,6 +362,7 @@ class FtRestClient(): "pair": pair, "timeframe": timeframe, "strategy": strategy, + "freqaimodel": freqaimodel, "timerange": timerange if timerange else '', }) diff --git a/scripts/ws_client.py b/scripts/ws_client.py old mode 100644 new mode 100755 diff --git a/setup.cfg b/setup.cfg index 60ec8a75f..b54b62619 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Operating System :: MacOS Operating System :: Unix Topic :: Office/Business :: Financial :: Investment diff --git a/setup.py b/setup.py index 894388554..f8b8b515c 100644 --- a/setup.py +++ b/setup.py @@ -8,21 +8,23 @@ hyperopt = [ 'scikit-learn', 'scikit-optimize>=0.7.0', 'filelock', - 'progressbar2', ] freqai = [ 'scikit-learn', + 'joblib', 'catboost; platform_machine != "aarch64"', 'lightgbm', - 'xgboost' + 'xgboost', + 'tensorboard' ] freqai_rl = [ 'torch', + 'gymnasium', 'stable-baselines3', - 'gym==0.21', - 'sb3-contrib' + 'sb3-contrib', + 'tqdm' ] hdf5 = [ @@ -32,14 +34,21 @@ hdf5 = [ develop = [ 'coveralls', - 'flake8', - 'flake8-tidy-imports', 'mypy', + 'ruff', + 'pre-commit', 'pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', 'pytest-random-order', + 'isort', + 'time-machine', + 'types-cachetools', + 'types-filelock', + 'types-requests', + 'types-tabulate', + 'types-python-dateutil' ] jupyter = [ @@ -60,10 +69,10 @@ setup( ], install_requires=[ # from requirements.txt - 'ccxt>=1.92.9', - 'SQLAlchemy', - 'python-telegram-bot>=13.4', - 'arrow>=0.17.0', + 'ccxt>=3.0.0', + 'SQLAlchemy>=2.0.6', + 'python-telegram-bot>=20.1', + 'arrow>=1.0.0', 'cachetools', 'requests', 'urllib3', @@ -84,6 +93,7 @@ setup( 'numpy', 'pandas', 'joblib>=1.2.0', + 'rich', 'pyarrow; platform_machine != "armv7l"', 'fastapi', 'pydantic>=1.8.0', @@ -93,7 +103,13 @@ setup( 'aiofiles', 'schedule', 'websockets', - 'janus' + 'janus', + 'ast-comments', + 'aiohttp', + 'cryptography', + 'httpx', + 'python-dateutil', + 'packaging', ], extras_require={ 'dev': all_extra, diff --git a/setup.sh b/setup.sh index 4cb504853..84f804021 100755 --- a/setup.sh +++ b/setup.sh @@ -25,7 +25,7 @@ function check_installed_python() { exit 2 fi - for v in 10 9 8 + for v in 11 10 9 8 do PYTHON="python3.${v}" which $PYTHON @@ -49,48 +49,49 @@ function updateenv() { source .env/bin/activate SYS_ARCH=$(uname -m) echo "pip install in-progress. Please wait..." - ${PYTHON} -m pip install --upgrade pip - read -p "Do you want to install dependencies for dev [y/N]? " + ${PYTHON} -m pip install --upgrade pip wheel setuptools + REQUIREMENTS_HYPEROPT="" + REQUIREMENTS_PLOT="" + REQUIREMENTS_FREQAI="" + REQUIREMENTS_FREQAI_RL="" + REQUIREMENTS=requirements.txt + + read -p "Do you want to install dependencies for development (Performs a full install with all dependencies) [y/N]? " dev=$REPLY if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS=requirements-dev.txt else - REQUIREMENTS=requirements.txt - fi - REQUIREMENTS_HYPEROPT="" - REQUIREMENTS_PLOT="" - read -p "Do you want to install plotting dependencies (plotly) [y/N]? " - if [[ $REPLY =~ ^[Yy]$ ]] - then - REQUIREMENTS_PLOT="-r requirements-plot.txt" - fi - if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then - echo "Detected Raspberry, installing cython, skipping hyperopt installation." - ${PYTHON} -m pip install --upgrade cython - else - # Is not Raspberry - read -p "Do you want to install hyperopt dependencies [y/N]? " + # requirements-dev.txt includes all the below requirements already, so further questions are pointless. + read -p "Do you want to install plotting dependencies (plotly) [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then - REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt" + REQUIREMENTS_PLOT="-r requirements-plot.txt" + fi + if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then + echo "Detected Raspberry, installing cython, skipping hyperopt installation." + ${PYTHON} -m pip install --upgrade cython + else + # Is not Raspberry + read -p "Do you want to install hyperopt dependencies [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt" + fi fi - fi - REQUIREMENTS_FREQAI="" - REQUIREMENTS_FREQAI_RL="" - read -p "Do you want to install dependencies for freqai [y/N]? " - dev=$REPLY - if [[ $REPLY =~ ^[Yy]$ ]] - then - REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517" - read -p "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]? " - dev=$REPLY + read -p "Do you want to install dependencies for freqai [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then - REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt" + REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517" + read -p "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt" + fi fi fi + install_talib ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} ${REQUIREMENTS_FREQAI} ${REQUIREMENTS_FREQAI_RL} if [ $? -ne 0 ]; then @@ -168,21 +169,18 @@ function install_macos() { if [[ $version -ge 9 ]]; then #Checks if python version >= 3.9 install_mac_newer_python_dependencies fi - install_talib } # Install bot Debian_ubuntu function install_debian() { sudo apt-get update sudo apt-get install -y gcc build-essential autoconf libtool pkg-config make wget git curl $(echo lib${PYTHON}-dev ${PYTHON}-venv) - install_talib } # Install bot RedHat_CentOS function install_redhat() { sudo yum update sudo yum install -y gcc gcc-c++ make autoconf libtool pkg-config wget git $(echo ${PYTHON}-devel | sed 's/\.//g') - install_talib } # Upgrade the bot @@ -191,26 +189,37 @@ function update() { updateenv } +function check_git_changes() { + if [ -z "$(git status --porcelain)" ]; then + echo "No changes in git directory" + return 1 + else + echo "Changes in git directory" + return 0 + fi +} + # Reset Develop or Stable branch function reset() { echo_block "Resetting branch and virtual env" if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ] then + if check_git_changes; then + read -p "Keep your local changes? (Otherwise will remove all changes you made!) [Y/n]? " + if [[ $REPLY =~ ^[Nn]$ ]]; then - read -p "Reset git branch? (This will remove all changes you made!) [y/N]? " - if [[ $REPLY =~ ^[Yy]$ ]]; then + git fetch -a - git fetch -a - - if [ "1" == $(git branch -vv | grep -c "* develop") ] - then - echo "- Hard resetting of 'develop' branch." - git reset --hard origin/develop - elif [ "1" == $(git branch -vv | grep -c "* stable") ] - then - echo "- Hard resetting of 'stable' branch." - git reset --hard origin/stable + if [ "1" == $(git branch -vv | grep -c "* develop") ] + then + echo "- Hard resetting of 'develop' branch." + git reset --hard origin/develop + elif [ "1" == $(git branch -vv | grep -c "* stable") ] + then + echo "- Hard resetting of 'stable' branch." + git reset --hard origin/stable + fi fi fi else @@ -249,7 +258,7 @@ function install() { install_redhat else echo "This script does not support your OS." - echo "If you have Python version 3.8 - 3.10, pip, virtualenv, ta-lib you can continue." + echo "If you have Python version 3.8 - 3.11, pip, virtualenv, ta-lib you can continue." echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell." sleep 10 fi diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 967dbe296..fe847e94b 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,12 +1,11 @@ import json import re -from datetime import datetime +from datetime import datetime, timedelta from io import BytesIO from pathlib import Path from unittest.mock import MagicMock, PropertyMock from zipfile import ZipFile -import arrow import pytest from freqtrade.commands import (start_backtesting_show, start_convert_data, start_convert_trades, @@ -14,7 +13,8 @@ from freqtrade.commands import (start_backtesting_show, start_convert_data, star start_hyperopt_show, start_install_ui, start_list_data, start_list_exchanges, start_list_markets, start_list_strategies, start_list_timeframes, start_new_strategy, start_show_trades, - start_test_pairlist, start_trading, start_webserver) + start_strategy_update, start_test_pairlist, start_trading, + start_webserver) from freqtrade.commands.db_commands import start_convert_db from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) @@ -24,7 +24,8 @@ from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks -from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_args, log_has, +from freqtrade.util import dt_floor_day, dt_now, dt_utc +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) from tests.conftest_trades import MOCK_TRADE_COUNT @@ -454,7 +455,7 @@ def test_list_markets(mocker, markets_static, capsys): assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE) assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(side_effect=ValueError)) + mocker.patch(f'{EXMS}.markets', PropertyMock(side_effect=ValueError)) # Test --one-column args = [ "list-markets", @@ -643,9 +644,7 @@ def test_download_data_keyboardInterrupt(mocker, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) args = [ "download-data", "--exchange", "binance", @@ -664,9 +663,7 @@ def test_download_data_timerange(mocker, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) args = [ "download-data", "--exchange", "binance", @@ -692,7 +689,7 @@ def test_download_data_timerange(mocker, markets): start_download_data(pargs) assert dl_mock.call_count == 1 # 20days ago - days_ago = arrow.get(arrow.now().shift(days=-20).date()).int_timestamp + days_ago = dt_floor_day(dt_now() - timedelta(days=20)).timestamp() assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago dl_mock.reset_mock() @@ -707,17 +704,14 @@ def test_download_data_timerange(mocker, markets): start_download_data(pargs) assert dl_mock.call_count == 1 - assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow( - 2020, 1, 1).int_timestamp + assert dl_mock.call_args_list[0][1]['timerange'].startts == int(dt_utc(2020, 1, 1).timestamp()) def test_download_data_no_markets(mocker, caplog): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker, id='binance') - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", "--exchange", "binance", @@ -733,9 +727,7 @@ def test_download_data_no_exchange(mocker, caplog): mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", ] @@ -751,9 +743,7 @@ def test_download_data_no_pairs(mocker): mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", "--exchange", @@ -771,9 +761,7 @@ def test_download_data_all_pairs(mocker, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) args = [ "download-data", "--exchange", @@ -810,9 +798,7 @@ def test_download_data_trades(mocker, caplog): convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", "--exchange", "kraken", @@ -843,9 +829,7 @@ def test_download_data_trades(mocker, caplog): def test_download_data_data_invalid(mocker): patch_exchange(mocker, id="kraken") - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "download-data", "--exchange", "kraken", @@ -862,9 +846,7 @@ def test_start_convert_trades(mocker, caplog): convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "trades-to-ohlcv", "--exchange", "kraken", @@ -971,7 +953,7 @@ def test_start_list_freqAI_models(capsys): def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): patch_exchange(mocker, mock_markets=True) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, exchange_has=MagicMock(return_value=True), get_tickers=tickers, ) @@ -1450,10 +1432,10 @@ def test_start_list_data(testdatadir, capsys): start_list_data(pargs) captured = capsys.readouterr() - assert "Found 5 pair / timeframe combinations." in captured.out - assert "\n| Pair | Timeframe | Type |\n" in captured.out - assert "\n| XRP/USDT | 1h | futures |\n" in captured.out - assert "\n| XRP/USDT | 1h, 8h | mark |\n" in captured.out + assert "Found 6 pair / timeframe combinations." in captured.out + assert "\n| Pair | Timeframe | Type |\n" in captured.out + assert "\n| XRP/USDT:USDT | 5m, 1h | futures |\n" in captured.out + assert "\n| XRP/USDT:USDT | 1h, 8h | mark |\n" in captured.out args = [ "list-data", @@ -1564,3 +1546,37 @@ def test_start_convert_db(mocker, fee, tmpdir, caplog): start_convert_db(pargs) assert db_target_file.is_file() + + +def test_start_strategy_updater(mocker, tmpdir): + sc_mock = mocker.patch('freqtrade.commands.strategy_utils_commands.start_conversion') + teststrats = Path(__file__).parent.parent / 'strategy/strats' + args = [ + "strategy-updater", + "--userdir", + str(tmpdir), + "--strategy-path", + str(teststrats), + ] + pargs = get_args(args) + pargs['config'] = None + start_strategy_update(pargs) + # Number of strategies in the test directory + assert sc_mock.call_count == 11 + + sc_mock.reset_mock() + args = [ + "strategy-updater", + "--userdir", + str(tmpdir), + "--strategy-path", + str(teststrats), + "--strategy-list", + "StrategyTestV3", + "StrategyTestV2" + ] + pargs = get_args(args) + pargs['config'] = None + start_strategy_update(pargs) + # Number of strategies in the test directory + assert sc_mock.call_count == 2 diff --git a/tests/conftest.py b/tests/conftest.py index 90608d047..66f331cae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,16 +3,14 @@ import json import logging import re from copy import deepcopy -from datetime import datetime, timedelta +from datetime import timedelta from pathlib import Path from typing import Optional from unittest.mock import MagicMock, Mock, PropertyMock -import arrow import numpy as np import pandas as pd import pytest -from telegram import Chat, Message, Update from freqtrade import constants from freqtrade.commands import Arguments @@ -24,6 +22,8 @@ from freqtrade.exchange.exchange import timeframe_to_minutes from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Order, Trade, init_db from freqtrade.resolvers import ExchangeResolver +from freqtrade.util import dt_ts +from freqtrade.util.datetime_helpers import dt_now from freqtrade.worker import Worker from tests.conftest_trades import (leverage_trade, mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, mock_trade_5, mock_trade_6, short_trade) @@ -40,6 +40,7 @@ np.seterr(all='raise') CURRENT_TEST_STRATEGY = 'StrategyTestV3' TRADE_SIDES = ('long', 'short') +EXMS = 'freqtrade.exchange.exchange.Exchange' def pytest_addoption(parser): @@ -145,22 +146,21 @@ def patch_exchange( mock_markets=True, mock_supported_modes=True ) -> None: - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) - mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) + mocker.patch(f'{EXMS}._load_async_markets', return_value={}) + mocker.patch(f'{EXMS}.validate_config', MagicMock()) + mocker.patch(f'{EXMS}.validate_timeframes', MagicMock()) + mocker.patch(f'{EXMS}.id', PropertyMock(return_value=id)) + mocker.patch(f'{EXMS}.name', PropertyMock(return_value=id.title())) + mocker.patch(f'{EXMS}.precisionMode', PropertyMock(return_value=2)) if mock_markets: if isinstance(mock_markets, bool): mock_markets = get_markets() - mocker.patch('freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=mock_markets)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=mock_markets)) if mock_supported_modes: mocker.patch( - f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_margin_pairs', + f'freqtrade.exchange.{id}.{id.capitalize()}._supported_trading_mode_margin_pairs', PropertyMock(return_value=[ (TradingMode.MARGIN, MarginMode.CROSS), (TradingMode.MARGIN, MarginMode.ISOLATED), @@ -170,10 +170,10 @@ def patch_exchange( ) if api_mock: - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._init_ccxt', return_value=api_mock) else: - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( + mocker.patch(f'{EXMS}._init_ccxt', MagicMock()) + mocker.patch(f'{EXMS}.timeframes', PropertyMock( return_value=['5m', '15m', '1h', '1d'])) @@ -182,7 +182,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='binance', patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) config['exchange']['name'] = id try: - exchange = ExchangeResolver.load_exchange(id, config, load_leverage_tiers=True) + exchange = ExchangeResolver.load_exchange(config, load_leverage_tiers=True) except ImportError: exchange = Exchange(config) return exchange @@ -241,7 +241,6 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: :return: FreqtradeBot """ patch_freqtradebot(mocker, config) - config['datadir'] = Path(config['datadir']) return FreqtradeBot(config) @@ -300,7 +299,7 @@ def create_mock_trades(fee, is_short: Optional[bool] = False, use_db: bool = Tru """ def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) is_short1 = is_short if is_short is not None else True @@ -333,11 +332,11 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): Create some fake trades ... """ if use_db: - Trade.query.session.rollback() + Trade.session.rollback() def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) @@ -367,7 +366,7 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): add_trade(trade) if use_db: - Trade.query.session.flush() + Trade.session.flush() def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool = True): @@ -376,7 +375,7 @@ def create_mock_trades_usdt(fee, is_short: Optional[bool] = False, use_db: bool """ def add_trade(trade): if use_db: - Trade.query.session.add(trade) + Trade.session.add(trade) else: LocalTrade.add_bt_trade(trade) @@ -413,6 +412,14 @@ def patch_gc(mocker) -> None: mocker.patch("freqtrade.main.gc_set_threshold") +@pytest.fixture(autouse=True) +def user_dir(mocker, tmpdir) -> Path: + user_dir = Path(tmpdir) / "user_data" + mocker.patch('freqtrade.configuration.configuration.create_userdata_dir', + return_value=user_dir) + return user_dir + + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: """ @@ -487,7 +494,6 @@ def get_default_conf(testdatadir): }, "exchange": { "name": "binance", - "enabled": True, "key": "key", "secret": "secret", "pair_whitelist": [ @@ -505,12 +511,12 @@ def get_default_conf(testdatadir): {"method": "StaticPairList"} ], "telegram": { - "enabled": True, + "enabled": False, "token": "token", "chat_id": "0", "notification_settings": {}, }, - "datadir": str(testdatadir), + "datadir": Path(testdatadir), "initial_state": "running", "db_url": "sqlite://", "user_data_dir": Path("user_data"), @@ -551,13 +557,6 @@ def get_default_conf_usdt(testdatadir): return configuration -@pytest.fixture -def update(): - _update = Update(0) - _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) - return _update - - @pytest.fixture def fee(): return MagicMock(return_value=0.0025) @@ -1665,8 +1664,8 @@ def limit_buy_order_open(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'datetime': arrow.utcnow().isoformat(), + 'timestamp': dt_ts(), + 'datetime': dt_now().isoformat(), 'price': 0.00001099, 'average': 0.00001099, 'amount': 90.99181073, @@ -1693,8 +1692,8 @@ def limit_buy_order_old(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, + 'datetime': (dt_now() - timedelta(minutes=601)).isoformat(), + 'timestamp': dt_ts(dt_now() - timedelta(minutes=601)), 'price': 0.00001099, 'amount': 90.99181073, 'filled': 0.0, @@ -1710,8 +1709,8 @@ def limit_sell_order_old(): 'type': 'limit', 'side': 'sell', 'symbol': 'ETH/BTC', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'timestamp': dt_ts(dt_now() - timedelta(minutes=601)), + 'datetime': (dt_now() - timedelta(minutes=601)).isoformat(), 'price': 0.00001099, 'amount': 90.99181073, 'filled': 0.0, @@ -1727,8 +1726,8 @@ def limit_buy_order_old_partial(): 'type': 'limit', 'side': 'buy', 'symbol': 'ETH/BTC', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'timestamp': dt_ts(dt_now() - timedelta(minutes=601)), + 'datetime': (dt_now() - timedelta(minutes=601)).isoformat(), 'price': 0.00001099, 'amount': 90.99181073, 'filled': 23.0, @@ -1758,8 +1757,8 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': 'AZNPFF-4AC4N-7MKTAT', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'timestamp': dt_ts(dt_now() - timedelta(minutes=601)), + 'datetime': (dt_now() - timedelta(minutes=601)).isoformat(), 'lastTradeTimestamp': None, 'status': 'canceled', 'symbol': 'LTC/USDT', @@ -1779,8 +1778,8 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'timestamp': dt_ts(dt_now() - timedelta(minutes=601)), + 'datetime': (dt_now() - timedelta(minutes=601)).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', 'type': 'limit', @@ -1800,8 +1799,8 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'timestamp': dt_ts(dt_now() - timedelta(minutes=601)), + 'datetime': (dt_now() - timedelta(minutes=601)).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', 'type': 'limit', @@ -1825,8 +1824,8 @@ def limit_sell_order_open(): 'type': 'limit', 'side': 'sell', 'symbol': 'mocked', - 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp * 1000, + 'datetime': dt_now().isoformat(), + 'timestamp': dt_ts(), 'price': 0.00001173, 'amount': 90.99181073, 'filled': 0.0, @@ -2488,8 +2487,8 @@ def buy_order_fee(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp * 1000, - 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), + 'timestamp': dt_ts(dt_now() - timedelta(minutes=601)), + 'datetime': (dt_now() - timedelta(minutes=601)).isoformat(), 'price': 0.245441, 'amount': 8.0, 'cost': 1.963528, @@ -2574,7 +2573,7 @@ def import_fails() -> None: realimport = builtins.__import__ def mockedimport(name, *args, **kwargs): - if name in ["filelock", 'systemd.journal', 'uvloop']: + if name in ["filelock", 'cysystemd.journal', 'uvloop']: raise ImportError(f"No module named '{name}'") return realimport(name, *args, **kwargs) @@ -2598,7 +2597,7 @@ def open_trade(): fee_open=0.0, fee_close=0.0, stake_amount=1, - open_date=arrow.utcnow().shift(minutes=-601).datetime, + open_date=dt_now() - timedelta(minutes=601), is_open=True ) trade.orders = [ @@ -2636,7 +2635,7 @@ def open_trade_usdt(): fee_open=0.0, fee_close=0.0, stake_amount=60.0, - open_date=arrow.utcnow().shift(minutes=-601).datetime, + open_date=dt_now() - timedelta(minutes=601), is_open=True ) trade.orders = [ @@ -2840,8 +2839,8 @@ def limit_buy_order_usdt_open(): 'type': 'limit', 'side': 'buy', 'symbol': 'mocked', - 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp * 1000, + 'datetime': dt_now().isoformat(), + 'timestamp': dt_ts(), 'price': 2.00, 'average': 2.00, 'amount': 30.0, @@ -2868,8 +2867,8 @@ def limit_sell_order_usdt_open(): 'type': 'limit', 'side': 'sell', 'symbol': 'mocked', - 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().int_timestamp * 1000, + 'datetime': dt_now().isoformat(), + 'timestamp': dt_ts(), 'price': 2.20, 'amount': 30.0, 'cost': 66.0, @@ -2895,8 +2894,8 @@ def market_buy_order_usdt(): 'type': 'market', 'side': 'buy', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'datetime': arrow.utcnow().isoformat(), + 'timestamp': dt_ts(), + 'datetime': dt_now().isoformat(), 'price': 2.00, 'amount': 30.0, 'filled': 30.0, @@ -2952,8 +2951,8 @@ def market_sell_order_usdt(): 'type': 'market', 'side': 'sell', 'symbol': 'mocked', - 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'datetime': arrow.utcnow().isoformat(), + 'timestamp': dt_ts(), + 'datetime': dt_now().isoformat(), 'price': 2.20, 'amount': 30.0, 'filled': 30.0, @@ -3109,7 +3108,7 @@ def funding_rate_history_octohourly(): @pytest.fixture(scope='function') def leverage_tiers(): return { - "1000SHIB/USDT": [ + "1000SHIB/USDT:USDT": [ { 'minNotional': 0, 'maxNotional': 50000, @@ -3160,7 +3159,7 @@ def leverage_tiers(): 'maintAmt': 654500.0 }, ], - "1INCH/USDT": [ + "1INCH/USDT:USDT": [ { 'minNotional': 0, 'maxNotional': 5000, @@ -3204,7 +3203,7 @@ def leverage_tiers(): 'maintAmt': 386940.0 }, ], - "AAVE/USDT": [ + "AAVE/USDT:USDT": [ { 'minNotional': 0, 'maxNotional': 5000, @@ -3248,7 +3247,7 @@ def leverage_tiers(): 'maintAmt': 386950.0 }, ], - "ADA/BUSD": [ + "ADA/BUSD:BUSD": [ { "minNotional": 0, "maxNotional": 100000, @@ -3292,7 +3291,7 @@ def leverage_tiers(): "maintAmt": 1527500.0 }, ], - 'BNB/BUSD': [ + 'BNB/BUSD:BUSD': [ { "minNotional": 0, # stake(before leverage) = 0 "maxNotional": 100000, # max stake(before leverage) = 5000 @@ -3336,7 +3335,7 @@ def leverage_tiers(): "maintAmt": 1527500.0 } ], - 'BNB/USDT': [ + 'BNB/USDT:USDT': [ { "minNotional": 0, # stake = 0.0 "maxNotional": 10000, # max_stake = 133.33333333333334 @@ -3401,7 +3400,7 @@ def leverage_tiers(): "maintAmt": 6233035.0 }, ], - 'BTC/USDT': [ + 'BTC/USDT:USDT': [ { "minNotional": 0, # stake = 0.0 "maxNotional": 50000, # max_stake = 400.0 @@ -3473,7 +3472,7 @@ def leverage_tiers(): "maintAmt": 1.997038E8 }, ], - "ZEC/USDT": [ + "ZEC/USDT:USDT": [ { 'minNotional': 0, 'maxNotional': 50000, diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 345e3c299..5e377f851 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -1,8 +1,8 @@ +from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock import pytest -from arrow import Arrow from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange @@ -18,6 +18,7 @@ from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_ calculate_underwater, combine_dataframes_with_mean, create_cum_profit) from freqtrade.exceptions import OperationalException +from freqtrade.util import dt_utc from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT @@ -98,7 +99,7 @@ def test_load_backtest_data_new_format(testdatadir): assert bt_data.equals(bt_data3) with pytest.raises(ValueError, match=r"File .* does not exist\."): - load_backtest_data(str("filename") + "nofile") + load_backtest_data("filename" + "nofile") with pytest.raises(ValueError, match=r"Unknown dataformat."): load_backtest_data(testdatadir / "backtest_results" / LAST_BT_RESULT_FN) @@ -162,25 +163,25 @@ def test_extract_trades_of_period(testdatadir): {'pair': [pair, pair, pair, pair], 'profit_ratio': [0.0, 0.1, -0.2, -0.5], 'profit_abs': [0.0, 1, -2, -5], - 'open_date': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime, - Arrow(2017, 11, 14, 9, 41, 0).datetime, - Arrow(2017, 11, 14, 14, 20, 0).datetime, - Arrow(2017, 11, 15, 3, 40, 0).datetime, + 'open_date': to_datetime([datetime(2017, 11, 13, 15, 40, 0, tzinfo=timezone.utc), + datetime(2017, 11, 14, 9, 41, 0, tzinfo=timezone.utc), + datetime(2017, 11, 14, 14, 20, 0, tzinfo=timezone.utc), + datetime(2017, 11, 15, 3, 40, 0, tzinfo=timezone.utc), ], utc=True ), - 'close_date': to_datetime([Arrow(2017, 11, 13, 16, 40, 0).datetime, - Arrow(2017, 11, 14, 10, 41, 0).datetime, - Arrow(2017, 11, 14, 15, 25, 0).datetime, - Arrow(2017, 11, 15, 3, 55, 0).datetime, + 'close_date': to_datetime([datetime(2017, 11, 13, 16, 40, 0, tzinfo=timezone.utc), + datetime(2017, 11, 14, 10, 41, 0, tzinfo=timezone.utc), + datetime(2017, 11, 14, 15, 25, 0, tzinfo=timezone.utc), + datetime(2017, 11, 15, 3, 55, 0, tzinfo=timezone.utc), ], utc=True) }) trades1 = extract_trades_of_period(data, trades) # First and last trade are dropped as they are out of range assert len(trades1) == 2 - assert trades1.iloc[0].open_date == Arrow(2017, 11, 14, 9, 41, 0).datetime - assert trades1.iloc[0].close_date == Arrow(2017, 11, 14, 10, 41, 0).datetime - assert trades1.iloc[-1].open_date == Arrow(2017, 11, 14, 14, 20, 0).datetime - assert trades1.iloc[-1].close_date == Arrow(2017, 11, 14, 15, 25, 0).datetime + assert trades1.iloc[0].open_date == datetime(2017, 11, 14, 9, 41, 0, tzinfo=timezone.utc) + assert trades1.iloc[0].close_date == datetime(2017, 11, 14, 10, 41, 0, tzinfo=timezone.utc) + assert trades1.iloc[-1].open_date == datetime(2017, 11, 14, 14, 20, 0, tzinfo=timezone.utc) + assert trades1.iloc[-1].close_date == datetime(2017, 11, 14, 15, 25, 0, tzinfo=timezone.utc) def test_analyze_trade_parallelism(testdatadir): @@ -420,7 +421,7 @@ def test_calculate_max_drawdown2(): -0.025782, 0.010400, 0.012374, 0.012467, 0.114741, 0.010303, 0.010088, -0.033961, 0.010680, 0.010886, -0.029274, 0.011178, 0.010693, 0.010711] - dates = [Arrow(2020, 1, 1).shift(days=i) for i in range(len(values))] + dates = [dt_utc(2020, 1, 1) + timedelta(days=i) for i in range(len(values))] df = DataFrame(zip(values, dates), columns=['profit', 'open_date']) # sort by profit and reset index df = df.sort_values('profit').reset_index(drop=True) @@ -454,8 +455,8 @@ def test_calculate_max_drawdown_abs(profits, relative, highd, lowd, result, resu [1000, 500, 1000, 11000, 10000] # absolute results [1000, 50%, 0%, 0%, ~9%] # Relative drawdowns """ - init_date = Arrow(2020, 1, 1) - dates = [init_date.shift(days=i) for i in range(len(profits))] + init_date = datetime(2020, 1, 1, tzinfo=timezone.utc) + dates = [init_date + timedelta(days=i) for i in range(len(profits))] df = DataFrame(zip(profits, dates), columns=['profit_abs', 'open_date']) # sort by profit and reset index df = df.sort_values('profit_abs').reset_index(drop=True) @@ -467,8 +468,8 @@ def test_calculate_max_drawdown_abs(profits, relative, highd, lowd, result, resu assert isinstance(drawdown, float) assert isinstance(drawdown_rel, float) - assert hdate == init_date.shift(days=highd) - assert ldate == init_date.shift(days=lowd) + assert hdate == init_date + timedelta(days=highd) + assert ldate == init_date + timedelta(days=lowd) # High must be before low assert hdate < ldate diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 760ad8b76..b37a2100d 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -294,8 +294,8 @@ def test_convert_trades_format(default_conf, testdatadir, tmpdir): @pytest.mark.parametrize('file_base,candletype', [ (['XRP_ETH-5m', 'XRP_ETH-1m'], CandleType.SPOT), - (['UNITTEST_USDT-1h-mark', 'XRP_USDT-1h-mark'], CandleType.MARK), - (['XRP_USDT-1h-futures'], CandleType.FUTURES), + (['UNITTEST_USDT_USDT-1h-mark', 'XRP_USDT_USDT-1h-mark'], CandleType.MARK), + (['XRP_USDT_USDT-1h-futures'], CandleType.FUTURES), ]) def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, candletype): tmpdir1 = Path(tmpdir) @@ -315,7 +315,10 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand files_new.append(file_new) default_conf['datadir'] = tmpdir1 - default_conf['pairs'] = ['XRP_ETH', 'XRP_USDT', 'UNITTEST_USDT'] + if candletype == CandleType.SPOT: + default_conf['pairs'] = ['XRP/ETH', 'XRP/USDT', 'UNITTEST/USDT'] + else: + default_conf['pairs'] = ['XRP/ETH:ETH', 'XRP/USDT:USDT', 'UNITTEST/USDT:USDT'] default_conf['timeframes'] = ['1m', '5m', '1h'] assert not file_new.exists() diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 4d6489f11..f19b15455 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -33,10 +33,10 @@ def test_datahandler_ohlcv_get_pairs(testdatadir): assert set(pairs) == {'UNITTEST/BTC'} pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK) - assert set(pairs) == {'UNITTEST/USDT', 'XRP/USDT'} + assert set(pairs) == {'UNITTEST/USDT:USDT', 'XRP/USDT:USDT'} pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.FUTURES) - assert set(pairs) == {'XRP/USDT'} + assert set(pairs) == {'XRP/USDT:USDT'} pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '1h', candle_type=CandleType.MARK) assert set(pairs) == {'UNITTEST/USDT:USDT'} @@ -104,11 +104,12 @@ def test_datahandler_ohlcv_get_available_data(testdatadir): paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.FUTURES) # Convert to set to avoid failures due to sorting assert set(paircombs) == { - ('UNITTEST/USDT', '1h', 'mark'), - ('XRP/USDT', '1h', 'futures'), - ('XRP/USDT', '1h', 'mark'), - ('XRP/USDT', '8h', 'mark'), - ('XRP/USDT', '8h', 'funding_rate'), + ('UNITTEST/USDT:USDT', '1h', 'mark'), + ('XRP/USDT:USDT', '5m', 'futures'), + ('XRP/USDT:USDT', '1h', 'futures'), + ('XRP/USDT:USDT', '1h', 'mark'), + ('XRP/USDT:USDT', '8h', 'mark'), + ('XRP/USDT:USDT', '8h', 'funding_rate'), } paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir, TradingMode.SPOT) @@ -142,7 +143,7 @@ def test_jsondatahandler_ohlcv_load(testdatadir, caplog): df = dh.ohlcv_load('XRP/ETH', '5m', 'spot') assert len(df) == 712 - df_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', candle_type="mark") + df_mark = dh.ohlcv_load('UNITTEST/USDT:USDT', '1h', candle_type="mark") assert len(df_mark) == 100 df_no_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', 'spot') @@ -251,7 +252,7 @@ def test_datahandler__check_empty_df(testdatadir, caplog): assert log_has_re(expected_text, caplog) -@pytest.mark.parametrize('datahandler', ['feather', 'parquet']) +@pytest.mark.parametrize('datahandler', ['parquet']) def test_datahandler_trades_not_supported(datahandler, testdatadir, ): dh = get_datahandler(testdatadir, datahandler) with pytest.raises(NotImplementedError): @@ -424,7 +425,7 @@ def test_hdf5datahandler_ohlcv_load_and_resave( # Data goes from 2018-01-10 - 2018-01-30 ('UNITTEST/BTC', '5m', 'spot', '', '2018-01-15', '2018-01-19'), # Mark data goes from to 2021-11-15 2021-11-19 - ('UNITTEST/USDT', '1h', 'mark', '-mark', '2021-11-16', '2021-11-18'), + ('UNITTEST/USDT:USDT', '1h', 'mark', '-mark', '2021-11-16', '2021-11-18'), ]) @pytest.mark.parametrize('datahandler', ['hdf5', 'feather', 'parquet']) def test_generic_datahandler_ohlcv_load_and_resave( @@ -495,6 +496,58 @@ def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir): assert unlinkmock.call_count == 2 +def test_featherdatahandler_trades_load(testdatadir): + dh = get_datahandler(testdatadir, 'feather') + trades = dh.trades_load('XRP/ETH') + assert isinstance(trades, list) + assert trades[0][0] == 1570752011620 + assert trades[-1][-1] == 0.1986231 + + trades1 = dh.trades_load('UNITTEST/NONEXIST') + assert trades1 == [] + + +def test_featherdatahandler_trades_store(testdatadir, tmpdir): + tmpdir1 = Path(tmpdir) + dh = get_datahandler(testdatadir, 'feather') + trades = dh.trades_load('XRP/ETH') + + dh1 = get_datahandler(tmpdir1, 'feather') + dh1.trades_store('XRP/NEW', trades) + file = tmpdir1 / 'XRP_NEW-trades.feather' + assert file.is_file() + # Load trades back + trades_new = dh1.trades_load('XRP/NEW') + + assert len(trades_new) == len(trades) + assert trades[0][0] == trades_new[0][0] + assert trades[0][1] == trades_new[0][1] + # assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense + assert trades[0][3] == trades_new[0][3] + assert trades[0][4] == trades_new[0][4] + assert trades[0][5] == trades_new[0][5] + assert trades[0][6] == trades_new[0][6] + assert trades[-1][0] == trades_new[-1][0] + assert trades[-1][1] == trades_new[-1][1] + # assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense + assert trades[-1][3] == trades_new[-1][3] + assert trades[-1][4] == trades_new[-1][4] + assert trades[-1][5] == trades_new[-1][5] + assert trades[-1][6] == trades_new[-1][6] + + +def test_featherdatahandler_trades_purge(mocker, testdatadir): + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) + dh = get_datahandler(testdatadir, 'feather') + assert not dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 0 + + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + assert dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 1 + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index e0c79d52a..0e10b5848 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -8,7 +8,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType, RunMode from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.plugins.pairlistmanager import PairListManager -from tests.conftest import generate_test_data, get_patched_exchange +from tests.conftest import EXMS, generate_test_data, get_patched_exchange @pytest.mark.parametrize('candle_type', [ @@ -223,7 +223,7 @@ def test_emit_df(mocker, default_conf, ohlcv_history): def test_refresh(mocker, default_conf): refresh_mock = MagicMock() - mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) + mocker.patch(f"{EXMS}.refresh_latest_ohlcv", refresh_mock) exchange = get_patched_exchange(mocker, default_conf, id="binance") timeframe = default_conf["timeframe"] @@ -281,7 +281,7 @@ def test_market(mocker, default_conf, markets): def test_ticker(mocker, default_conf, tickers): ticker_mock = MagicMock(return_value=tickers()['ETH/BTC']) - mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) + mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock) exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, exchange) res = dp.ticker('ETH/BTC') @@ -290,7 +290,7 @@ def test_ticker(mocker, default_conf, tickers): assert res['symbol'] == 'ETH/BTC' ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found')) - mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) + mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock) exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, exchange) res = dp.ticker('UNITTEST/BTC') @@ -301,7 +301,7 @@ def test_current_whitelist(mocker, default_conf, tickers): # patch default conf to volumepairlist default_conf['pairlists'][0] = {'method': 'VolumePairList', "number_assets": 5} - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, exchange_has=MagicMock(return_value=True), get_tickers=tickers) exchange = get_patched_exchange(mocker, default_conf) @@ -437,6 +437,7 @@ def test_dp__add_external_df(default_conf_usdt): # Add the same dataframe again - dataframe size shall not change. res = dp._add_external_df('ETH/USDT', df, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) assert len(df) == 24 @@ -446,6 +447,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df2, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) assert len(df) == 48 @@ -455,6 +457,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df3, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is True + assert isinstance(res[1], int) assert res[1] == 0 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) # New length = 48 + 12 (since we have a 12 hour offset). @@ -478,6 +481,7 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is False # 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00 + assert isinstance(res[1], int) assert res[1] == 36 df, _ = dp.get_producer_df('ETH/USDT', timeframe, CandleType.SPOT) # New length = 61 + 1 @@ -488,4 +492,5 @@ def test_dp__add_external_df(default_conf_usdt): res = dp._add_external_df('ETH/USDT', df4, last_analyzed, timeframe, CandleType.SPOT) assert res[0] is False # 36 hours - from 2022-01-03 12:00:00+00:00 to 2022-01-05 00:00:00+00:00 + assert isinstance(res[1], int) assert res[1] == 0 diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py old mode 100755 new mode 100644 index e33ed4955..810e2c53b --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -18,8 +18,9 @@ def entryexitanalysis_cleanup() -> None: Backtesting.cleanup() -def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmpdir, capsys): +def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, user_dir, capsys): caplog.set_level(logging.INFO) + (user_dir / 'backtest_results').mkdir(parents=True, exist_ok=True) default_conf.update({ "use_exit_signal": True, @@ -80,7 +81,7 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp 'backtesting', '--config', 'config.json', '--datadir', str(testdatadir), - '--user-data-dir', str(tmpdir), + '--user-data-dir', str(user_dir), '--timeframe', '5m', '--timerange', '1515560100-1517287800', '--export', 'signals', @@ -98,7 +99,7 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp 'backtesting-analysis', '--config', 'config.json', '--datadir', str(testdatadir), - '--user-data-dir', str(tmpdir), + '--user-data-dir', str(user_dir), ] # test group 0 and indicator list @@ -190,9 +191,27 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp assert '1' in captured.out assert '2.5' in captured.out + # test group 5 + args = get_args(base_args + ['--analysis-groups', "5"]) + start_analysis_entries_exits(args) + captured = capsys.readouterr() + assert 'exit_signal' in captured.out + assert 'roi' in captured.out + assert 'stop_loss' in captured.out + assert 'trailing_stop_loss' in captured.out + # test date filtering - args = get_args(base_args + ['--timerange', "20180129-20180130"]) + args = get_args(base_args + + ['--analysis-groups', "0", "1", "2", + '--timerange', "20180129-20180130"] + ) start_analysis_entries_exits(args) captured = capsys.readouterr() assert 'enter_tag_long_a' in captured.out assert 'enter_tag_long_b' not in captured.out + + # Due to the backtest mock, there's no rejected signals generated. + args = get_args(base_args + ['--rejected-signals']) + start_analysis_entries_exits(args) + captured = capsys.readouterr() + assert 'no rejected signals' in captured.out diff --git a/tests/data/test_history.py b/tests/data/test_history.py index b985666cc..e397c97c1 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -6,7 +6,6 @@ from pathlib import Path from shutil import copyfile from unittest.mock import MagicMock, PropertyMock -import arrow import pytest from pandas import DataFrame from pandas.testing import assert_frame_equal @@ -26,7 +25,8 @@ from freqtrade.enums import CandleType from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver -from tests.conftest import (CURRENT_TEST_STRATEGY, get_patched_exchange, log_has, log_has_re, +from freqtrade.util import dt_utc +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_patched_exchange, log_has, log_has_re, patch_exchange) @@ -66,7 +66,7 @@ def test_load_data_7min_timeframe(caplog, testdatadir) -> None: def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history) file = testdatadir / 'UNITTEST_BTC-1m.json' load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) assert file.is_file() @@ -77,12 +77,12 @@ def test_load_data_1min_timeframe(ohlcv_history, mocker, caplog, testdatadir) -> def test_load_data_mark(ohlcv_history, mocker, caplog, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history) - file = testdatadir / 'futures/UNITTEST_USDT-1h-mark.json' + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history) + file = testdatadir / 'futures/UNITTEST_USDT_USDT-1h-mark.json' load_data(datadir=testdatadir, timeframe='1h', pairs=['UNITTEST/BTC'], candle_type='mark') assert file.is_file() assert not log_has( - 'Download history data for pair: "UNITTEST/USDT", interval: 1m ' + 'Download history data for pair: "UNITTEST/USDT:USDT", interval: 1m ' 'and store in None.', caplog ) @@ -109,7 +109,7 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, Test load_pair_history() with 1 min timeframe """ tmpdir1 = Path(tmpdir) - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) file = tmpdir1 / 'MEME_BTC-1m.json' @@ -191,14 +191,13 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None: test_data = None test_filename = testdatadir.joinpath('UNITTEST_BTC-1m.json') - with open(test_filename, "rt") as file: + with test_filename.open("rt") as file: test_data = json.load(file) test_data_df = ohlcv_to_dataframe(test_data, '1m', 'UNITTEST/BTC', fill_missing=False, drop_incomplete=False) # now = last cached item + 1 hour now_ts = test_data[-1][0] / 1000 + 60 * 60 - mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts)) # timeframe starts earlier than the cached data # should fully update data @@ -277,7 +276,7 @@ def test_download_pair_history( subdir, file_tail ) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ohlcv_history_list) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) tmpdir1 = Path(tmpdir) file1_1 = tmpdir1 / f'{subdir}MEME_BTC-1m{file_tail}.json' @@ -328,7 +327,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: json_dump_mock = mocker.patch( 'freqtrade.data.history.jsondatahandler.JsonDataHandler.ohlcv_store', return_value=None) - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) + mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) _download_pair_history(datadir=testdatadir, exchange=exchange, pair="UNITTEST/BTC", timeframe='1m', candle_type='spot') @@ -340,7 +339,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', + mocker.patch(f'{EXMS}.get_historic_ohlcv', side_effect=Exception('File Error')) tmpdir1 = Path(tmpdir) exchange = get_patched_exchange(mocker, default_conf) @@ -353,10 +352,10 @@ def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdi def test_load_partial_missing(testdatadir, caplog) -> None: # Make sure we start fresh - test missing data at start - start = arrow.get('2018-01-01T00:00:00') - end = arrow.get('2018-01-11T00:00:00') + start = dt_utc(2018, 1, 1) + end = dt_utc(2018, 1, 11) data = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20, - timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp)) + timerange=TimeRange('date', 'date', start.timestamp(), end.timestamp())) assert log_has( 'Using indicator startup period: 20 ...', caplog ) @@ -369,16 +368,16 @@ def test_load_partial_missing(testdatadir, caplog) -> None: caplog) # Make sure we start fresh - test missing data at end caplog.clear() - start = arrow.get('2018-01-10T00:00:00') - end = arrow.get('2018-02-20T00:00:00') + start = dt_utc(2018, 1, 10) + end = dt_utc(2018, 2, 20) data = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], - timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp)) + timerange=TimeRange('date', 'date', start.timestamp(), end.timestamp())) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(data['UNITTEST/BTC']) # Shift endtime with +5 - end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]) + end_real = data['UNITTEST/BTC'].iloc[-1, 0].to_pydatetime() assert log_has(f'UNITTEST/BTC, spot, 5m, ' f'data ends at {end_real.strftime(DATETIME_PRINT_FORMAT)}', caplog) @@ -409,7 +408,7 @@ def test_init_with_refresh(default_conf, mocker) -> None: def test_file_dump_json_tofile(testdatadir) -> None: - file = testdatadir / 'test_{id}.json'.format(id=str(uuid.uuid4())) + file = testdatadir / f'test_{uuid.uuid4()}.json' data = {'bar': 'foo'} # check the file we will create does not exist @@ -506,9 +505,7 @@ def test_refresh_backtest_ohlcv_data( mocker, default_conf, markets, caplog, testdatadir, trademode, callcount): dl_mock = mocker.patch('freqtrade.data.history.history_utils._download_pair_history', MagicMock()) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "unlink", MagicMock()) @@ -531,9 +528,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): MagicMock()) ex = get_patched_exchange(mocker, default_conf) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) timerange = TimeRange.parse_timerange("20190101-20190102") unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"], timeframes=["1m", "5m"], @@ -551,9 +546,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir): dl_mock = mocker.patch('freqtrade.data.history.history_utils._download_trades_history', MagicMock()) - mocker.patch( - 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "unlink", MagicMock()) @@ -577,8 +570,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad tmpdir) -> None: tmpdir1 = Path(tmpdir) ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history)) - mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', - ght_mock) + mocker.patch(f'{EXMS}.get_historic_trades', ght_mock) exchange = get_patched_exchange(mocker, default_conf) file1 = tmpdir1 / 'ETH_BTC-trades.json.gz' data_handler = get_datahandler(tmpdir1, data_format='jsongz') @@ -604,8 +596,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad file1.unlink() - mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', - MagicMock(side_effect=ValueError)) + mocker.patch(f'{EXMS}.get_historic_trades', MagicMock(side_effect=ValueError)) assert not _download_trades_history(data_handler=data_handler, exchange=exchange, pair='ETH/BTC') @@ -615,8 +606,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad copyfile(testdatadir / file2.name, file2) ght_mock.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', - ght_mock) + mocker.patch(f'{EXMS}.get_historic_trades', ght_mock) # Since before first start date since_time = int(trades_history[0][0] // 1000) - 500 timerange = TimeRange('date', None, since_time, 0) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 1b0191fda..4829dd035 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -3,9 +3,9 @@ import logging import math +from datetime import timedelta from unittest.mock import MagicMock -import arrow import numpy as np import pytest from pandas import DataFrame @@ -14,7 +14,8 @@ from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.enums import ExitType from freqtrade.exceptions import OperationalException -from tests.conftest import get_patched_freqtradebot, log_has +from freqtrade.util.datetime_helpers import dt_ts, dt_utc +from tests.conftest import EXMS, get_patched_freqtradebot, log_has from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset) @@ -27,7 +28,7 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, # 5) Stoploss and sell are hit. should sell on stoploss #################################################################### -tests_start_time = arrow.get(2018, 10, 3) +tests_start_time = dt_utc(2018, 10, 3) timeframe_in_minute = 60 # End helper functions @@ -139,7 +140,7 @@ def test_adjust(mocker, edge_conf): assert (edge.adjust(pairs) == ['E/F', 'C/D']) -def test_stoploss(mocker, edge_conf): +def test_edge_get_stoploss(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( @@ -150,10 +151,10 @@ def test_stoploss(mocker, edge_conf): } )) - assert edge.stoploss('E/F') == -0.01 + assert edge.get_stoploss('E/F') == -0.01 -def test_nonexisting_stoploss(mocker, edge_conf): +def test_nonexisting_get_stoploss(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( @@ -162,7 +163,7 @@ def test_nonexisting_stoploss(mocker, edge_conf): } )) - assert edge.stoploss('N/O') == -0.1 + assert edge.get_stoploss('N/O') == -0.1 def test_edge_stake_amount(mocker, edge_conf): @@ -220,7 +221,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): heartbeat = edge_conf['edge']['process_throttle_secs'] # should not recalculate if heartbeat not reached - edge._last_updated = arrow.utcnow().int_timestamp - heartbeat + 1 + edge._last_updated = dt_ts() - heartbeat + 1 assert edge.calculate(edge_conf['exchange']['pair_whitelist']) is False @@ -232,7 +233,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', NEOBTC = [ [ - tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000, + dt_ts(tests_start_time + timedelta(minutes=(x * timeframe_in_minute))), math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -244,7 +245,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', base = 0.002 LTCBTC = [ [ - tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000, + dt_ts(tests_start_time + timedelta(minutes=(x * timeframe_in_minute))), math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -261,19 +262,19 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', def test_edge_process_downloaded_data(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001)) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) assert edge.calculate(edge_conf['exchange']['pair_whitelist']) assert len(edge._cached_pairs) == 2 - assert edge._last_updated <= arrow.utcnow().int_timestamp + 2 + assert edge._last_updated <= dt_ts() + 2 def test_edge_process_no_data(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001)) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) mocker.patch('freqtrade.edge.edge_positioning.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -286,7 +287,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + mocker.patch(f'{EXMS}.get_fee', return_value=0.001) mocker.patch('freqtrade.edge.edge_positioning.refresh_data', ) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) # Return empty @@ -303,7 +304,7 @@ def test_edge_process_no_pairs(mocker, edge_conf, caplog): mocker.patch('freqtrade.freqtradebot.validate_config_consistency') freqtrade = get_patched_freqtradebot(mocker, edge_conf) - fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + fee_mock = mocker.patch(f'{EXMS}.get_fee', return_value=0.001) mocker.patch('freqtrade.edge.edge_positioning.refresh_data') mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) # Return empty @@ -319,7 +320,7 @@ def test_edge_process_no_pairs(mocker, edge_conf, caplog): def test_edge_init_error(mocker, edge_conf,): edge_conf['stake_amount'] = 0.5 - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.001)) with pytest.raises(OperationalException, match='Edge works only with unlimited stake amount'): get_patched_freqtradebot(mocker, edge_conf) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 189f0488d..d44dae00d 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -7,10 +7,23 @@ import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException -from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re +from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers +@pytest.mark.parametrize('side,type,time_in_force,expected', [ + ('buy', 'limit', 'gtc', {'timeInForce': 'GTC'}), + ('buy', 'limit', 'IOC', {'timeInForce': 'IOC'}), + ('buy', 'market', 'IOC', {}), + ('buy', 'limit', 'PO', {'timeInForce': 'PO'}), + ('sell', 'limit', 'PO', {'timeInForce': 'PO'}), + ('sell', 'market', 'PO', {}), + ]) +def test__get_params_binance(default_conf, mocker, side, type, time_in_force, expected): + exchange = get_patched_exchange(mocker, default_conf, id='binance') + assert exchange._get_params(side, type, 1, False, time_in_force) == expected + + @pytest.mark.parametrize('trademode', [TradingMode.FUTURES, TradingMode.SPOT]) @pytest.mark.parametrize('limitratio,expected,side', [ (None, 220 * 0.99, "sell"), @@ -20,7 +33,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers (0.99, 220 * 1.01, "buy"), (0.98, 220 * 1.02, "buy"), ]) -def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode): +def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop' @@ -34,13 +47,13 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side default_conf['dry_run'] = False default_conf['margin_mode'] = MarginMode.ISOLATED default_conf['trading_mode'] = trademode - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - with pytest.raises(OperationalException): - order = exchange.stoploss( + with pytest.raises(InvalidOrderException): + order = exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=190, @@ -50,11 +63,11 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side ) api_mock.create_order.reset_mock() - order_types = {'stoploss': 'limit'} + order_types = {'stoploss': 'limit', 'stoploss_price_type': 'mark'} if limitratio is not None: order_types.update({'stoploss_on_exchange_limit_ratio': limitratio}) - order = exchange.stoploss( + order = exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, @@ -75,14 +88,14 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side if trademode == TradingMode.SPOT: params_dict = {'stopPrice': 220} else: - params_dict = {'stopPrice': 220, 'reduceOnly': True} + params_dict = {'stopPrice': 220, 'reduceOnly': True, 'workingType': 'MARK_PRICE'} assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict # test exception handling with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss( + exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, @@ -94,7 +107,7 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss( + exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, @@ -104,22 +117,22 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", - "stoploss", "create_order", retries=1, + "create_stoploss", "create_order", retries=1, pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side, leverage=1.0) -def test_stoploss_order_dry_run_binance(default_conf, mocker): +def test_create_stoploss_order_dry_run_binance(default_conf, mocker): api_mock = MagicMock() order_type = 'stop_loss_limit' default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - with pytest.raises(OperationalException): - order = exchange.stoploss( + with pytest.raises(InvalidOrderException): + order = exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=190, @@ -130,7 +143,7 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss( + order = exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, @@ -495,7 +508,8 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers for key, value in leverage_tiers.items(): v = exchange._leverage_tiers[key] assert isinstance(v, list) - assert len(v) == len(value) + # Assert if conftest leverage tiers have less or equal tiers than the exchange + assert len(v) >= len(value) def test_additional_exchange_init_binance(default_conf, mocker): @@ -522,8 +536,15 @@ def test__set_leverage_binance(mocker, default_conf): api_mock.set_leverage = MagicMock() type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) default_conf['dry_run'] = False - exchange = get_patched_exchange(mocker, default_conf, id="binance") - exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange._set_leverage(3.2, 'BTC/USDT:USDT') + 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 + assert api_mock.set_leverage.call_args_list[0][1]['symbol'] == 'BTC/USDT:USDT' ccxt_exceptionhandlers( mocker, @@ -534,7 +555,6 @@ def test__set_leverage_binance(mocker, default_conf): "set_leverage", pair="XRP/USDT", leverage=5.0, - trading_mode=TradingMode.FUTURES ) @@ -575,25 +595,13 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) -@pytest.mark.parametrize("trading_mode,margin_mode,config", [ - ("spot", "", {}), - ("margin", "cross", {"options": {"defaultType": "margin"}}), - ("futures", "isolated", {"options": {"defaultType": "future"}}), -]) -def test__ccxt_config(default_conf, mocker, trading_mode, margin_mode, config): - default_conf['trading_mode'] = trading_mode - default_conf['margin_mode'] = margin_mode - exchange = get_patched_exchange(mocker, default_conf, id="binance") - assert exchange._ccxt_config == config - - @pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [ - ("BNB/BUSD", 0.0, 0.025, 0), - ("BNB/USDT", 100.0, 0.0065, 0), - ("BTC/USDT", 170.30, 0.004, 0), - ("BNB/BUSD", 999999.9, 0.1, 27500.0), - ("BNB/USDT", 5000000.0, 0.15, 233035.0), - ("BTC/USDT", 600000000, 0.5, 1.997038E8), + ("BNB/BUSD:BUSD", 0.0, 0.025, 0), + ("BNB/USDT:USDT", 100.0, 0.0065, 0), + ("BTC/USDT:USDT", 170.30, 0.004, 0), + ("BNB/BUSD:BUSD", 999999.9, 0.1, 27500.0), + ("BNB/USDT:USDT", 5000000.0, 0.15, 233035.0), + ("BTC/USDT:USDT", 600000000, 0.5, 1.997038E8), ]) def test_get_maintenance_ratio_and_amt_binance( default_conf, @@ -604,7 +612,7 @@ def test_get_maintenance_ratio_and_amt_binance( mm_ratio, amt, ): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._leverage_tiers = leverage_tiers (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) diff --git a/tests/exchange/test_bitpanda.py b/tests/exchange/test_bitpanda.py index 4bd168e7e..de44be986 100644 --- a/tests/exchange/test_bitpanda.py +++ b/tests/exchange/test_bitpanda.py @@ -1,7 +1,7 @@ from datetime import datetime from unittest.mock import MagicMock -from tests.conftest import get_patched_exchange +from tests.conftest import EXMS, get_patched_exchange def test_get_trades_for_order(default_conf, mocker): @@ -9,7 +9,7 @@ def test_get_trades_for_order(default_conf, mocker): order_id = 'ABCD-ABCD' since = datetime(2018, 5, 5, 0, 0, 0) default_conf["dry_run"] = False - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) api_mock = MagicMock() api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV', diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py new file mode 100644 index 000000000..d0d5114a1 --- /dev/null +++ b/tests/exchange/test_bybit.py @@ -0,0 +1,74 @@ +from datetime import datetime, timezone +from unittest.mock import MagicMock + +from freqtrade.enums.marginmode import MarginMode +from freqtrade.enums.tradingmode import TradingMode +from freqtrade.exchange.exchange_utils import timeframe_to_msecs +from tests.conftest import get_mock_coro, get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers + + +def test_additional_exchange_init_bybit(default_conf, mocker): + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + api_mock = MagicMock() + api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False}) + get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + assert api_mock.set_position_mode.call_count == 1 + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit', + "additional_exchange_init", "set_position_mode") + + +async def test_bybit_fetch_funding_rate(default_conf, mocker): + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + api_mock = MagicMock() + api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[]) + exchange = get_patched_exchange(mocker, default_conf, id='bybit', api_mock=api_mock) + limit = 200 + # Test fetch_funding_rate_history (current data) + await exchange._fetch_funding_rate_history( + pair='BTC/USDT:USDT', + timeframe='4h', + limit=limit, + ) + + assert api_mock.fetch_funding_rate_history.call_count == 1 + assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' + kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] + assert kwargs['params'] == {} + assert kwargs['since'] is None + + api_mock.fetch_funding_rate_history.reset_mock() + since_ms = 1610000000000 + since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit) + # Test fetch_funding_rate_history (current data) + await exchange._fetch_funding_rate_history( + pair='BTC/USDT:USDT', + timeframe='4h', + limit=limit, + since_ms=since_ms, + ) + + assert api_mock.fetch_funding_rate_history.call_count == 1 + assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' + kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] + assert kwargs['params'] == {'until': since_ms_end} + assert kwargs['since'] == since_ms + + +def test_bybit_get_funding_fees(default_conf, mocker): + now = datetime.now(timezone.utc) + exchange = get_patched_exchange(mocker, default_conf, id='bybit') + exchange._fetch_and_calculate_funding_fees = MagicMock() + exchange.get_funding_fees('BTC/USDT:USDT', 1, False, now) + assert exchange._fetch_and_calculate_funding_fees.call_count == 0 + + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + exchange = get_patched_exchange(mocker, default_conf, id='bybit') + exchange._fetch_and_calculate_funding_fees = MagicMock() + exchange.get_funding_fees('BTC/USDT:USDT', 1, False, now) + + assert exchange._fetch_and_calculate_funding_fees.call_count == 1 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index e721ee2c9..6f5987202 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -12,11 +12,12 @@ from typing import Tuple import pytest +from freqtrade.constants import Config from freqtrade.enums import CandleType from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange.exchange import Exchange, timeframe_to_msecs from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import get_default_conf_usdt +from tests.conftest import EXMS, get_default_conf_usdt EXCHANGE_FIXTURE_TYPE = Tuple[Exchange, str] @@ -31,20 +32,66 @@ EXCHANGES = { 'leverage_tiers_public': False, 'leverage_in_spot_market': False, }, - # 'binance': { - # 'pair': 'BTC/USDT', - # 'stake_currency': 'USDT', - # 'hasQuoteVolume': True, - # 'timeframe': '5m', - # 'futures': True, - # 'leverage_tiers_public': False, - # 'leverage_in_spot_market': False, - # }, + 'binance': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'use_ci_proxy': True, + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'futures': True, + 'futures_pair': 'BTC/USDT:USDT', + 'hasQuoteVolumeFutures': True, + 'leverage_tiers_public': False, + 'leverage_in_spot_market': False, + 'sample_order': [{ + "symbol": "SOLUSDT", + "orderId": 3551312894, + "orderListId": -1, + "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", + "transactTime": 1674493798550, + "price": "15.50000000", + "origQty": "1.10000000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "workingTime": 1674493798550, + "fills": [], + "selfTradePreventionMode": "NONE", + }] + }, + 'binanceus': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'futures': False, + 'sample_order': [{ + "symbol": "SOLUSDT", + "orderId": 3551312894, + "orderListId": -1, + "clientOrderId": "x-R4DD3S8297c73a11ccb9dc8f2811ba", + "transactTime": 1674493798550, + "price": "15.50000000", + "origQty": "1.10000000", + "executedQty": "0.00000000", + "cummulativeQuoteQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "BUY", + "workingTime": 1674493798550, + "fills": [], + "selfTradePreventionMode": "NONE", + }] + }, 'kraken': { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': True, }, @@ -52,42 +99,168 @@ EXCHANGES = { 'pair': 'XRP/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': True, + 'sample_order': [ + {'id': '63d6742d0adc5570001d2bbf7'}, # create order + { + 'id': '63d6742d0adc5570001d2bbf7', + 'symbol': 'SOL-USDT', + 'opType': 'DEAL', + 'type': 'limit', + 'side': 'buy', + 'price': '15.5', + 'size': '1.1', + 'funds': '0', + 'dealFunds': '17.05', + 'dealSize': '1.1', + 'fee': '0.000065252', + 'feeCurrency': 'USDT', + 'stp': '', + 'stop': '', + 'stopTriggered': False, + 'stopPrice': '0', + 'timeInForce': 'GTC', + 'postOnly': False, + 'hidden': False, + 'iceberg': False, + 'visibleSize': '0', + 'cancelAfter': 0, + 'channel': 'API', + 'clientOid': '0a053870-11bf-41e5-be61-b272a4cb62e1', + 'remark': None, + 'tags': 'partner:ccxt', + 'isActive': False, + 'cancelExist': False, + 'createdAt': 1674493798550, + 'tradeType': 'TRADE' + }], }, - 'gateio': { + 'gate': { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': True, 'futures_pair': 'BTC/USDT:USDT', + 'hasQuoteVolumeFutures': True, 'leverage_tiers_public': True, 'leverage_in_spot_market': True, + 'sample_order': [ + { + "id": "276266139423", + "text": "apiv4", + "create_time": "1674493798", + "update_time": "1674493798", + "create_time_ms": "1674493798550", + "update_time_ms": "1674493798550", + "status": "closed", + "currency_pair": "SOL_USDT", + "type": "limit", + "account": "spot", + "side": "buy", + "amount": "1.1", + "price": "15.5", + "time_in_force": "gtc", + "iceberg": "0", + "left": "0", + "fill_price": "17.05", + "filled_total": "17.05", + "avg_deal_price": "15.5", + "fee": "0.0000018", + "fee_currency": "SOL", + "point_fee": "0", + "gt_fee": "0", + "gt_maker_fee": "0", + "gt_taker_fee": "0.0015", + "gt_discount": True, + "rebated_fee": "0", + "rebated_fee_currency": "USDT" + }, + { + # market order + 'id': '276401180529', + 'text': 'apiv4', + 'create_time': '1674493798', + 'update_time': '1674493798', + 'create_time_ms': '1674493798550', + 'update_time_ms': '1674493798550', + 'status': 'cancelled', + 'currency_pair': 'SOL_USDT', + 'type': 'market', + 'account': 'spot', + 'side': 'buy', + 'amount': '17.05', + 'price': '0', + 'time_in_force': 'ioc', + 'iceberg': '0', + 'left': '0.0000000016228', + 'fill_price': '17.05', + 'filled_total': '17.05', + 'avg_deal_price': '15.5', + 'fee': '0', + 'fee_currency': 'SOL', + 'point_fee': '0.0199999999967544', + 'gt_fee': '0', + 'gt_maker_fee': '0', + 'gt_taker_fee': '0', + 'gt_discount': False, + 'rebated_fee': '0', + 'rebated_fee_currency': 'USDT' + } + ], }, 'okx': { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', + 'futures': True, + 'futures_pair': 'BTC/USDT:USDT', + 'hasQuoteVolumeFutures': False, + 'leverage_tiers_public': True, + 'leverage_in_spot_market': True, + }, + 'bybit': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '1h', 'futures_pair': 'BTC/USDT:USDT', 'futures': True, 'leverage_tiers_public': True, 'leverage_in_spot_market': True, + 'sample_order': [ + { + "orderId": "1274754916287346280", + "orderLinkId": "1666798627015730", + "symbol": "SOLUSDT", + "createTime": "1674493798550", + "orderPrice": "15.5", + "orderQty": "1.1", + "orderType": "LIMIT", + "side": "BUY", + "status": "NEW", + "timeInForce": "GTC", + "accountId": "5555555", + "execQty": "0", + "orderCategory": "0" + } + ] }, 'huobi': { - 'pair': 'BTC/USDT', - 'stake_currency': 'USDT', + 'pair': 'ETH/BTC', + 'stake_currency': 'BTC', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'futures': False, }, 'bitvavo': { 'pair': 'BTC/EUR', 'stake_currency': 'EUR', 'hasQuoteVolume': True, - 'timeframe': '5m', + 'timeframe': '1h', 'leverage_tiers_public': False, 'leverage_in_spot_market': False, }, @@ -106,20 +279,41 @@ def exchange_conf(): return config +def set_test_proxy(config: Config, use_proxy: bool) -> Config: + # Set proxy to test in CI. + import os + if use_proxy and (proxy := os.environ.get('CI_WEB_PROXY')): + config1 = deepcopy(config) + config1['exchange']['ccxt_config'] = { + "aiohttp_proxy": proxy, + 'proxies': { + 'https': proxy, + 'http': proxy, + } + } + return config1 + + return config + + @pytest.fixture(params=EXCHANGES, scope="class") def exchange(request, exchange_conf): + exchange_conf = set_test_proxy( + exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) exchange_conf['exchange']['name'] = request.param exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] - exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True) + exchange = ExchangeResolver.load_exchange(exchange_conf, validate=True) yield exchange, request.param @pytest.fixture(params=EXCHANGES, scope="class") def exchange_futures(request, exchange_conf, class_mocker): - if not EXCHANGES[request.param].get('futures') is True: + if EXCHANGES[request.param].get('futures') is not True: yield None, request.param else: + exchange_conf = set_test_proxy( + exchange_conf, EXCHANGES[request.param].get('use_ci_proxy', False)) exchange_conf = deepcopy(exchange_conf) exchange_conf['exchange']['name'] = request.param exchange_conf['trading_mode'] = 'futures' @@ -128,15 +322,15 @@ def exchange_futures(request, exchange_conf, class_mocker): class_mocker.patch( 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') - class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') + class_mocker.patch(f'{EXMS}.fetch_trading_fees') class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init') - class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers', - return_value=None) - class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers') + class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init') + class_mocker.patch(f'{EXMS}.load_cached_leverage_tiers', return_value=None) + class_mocker.patch(f'{EXMS}.cache_leverage_tiers') exchange = ExchangeResolver.load_exchange( - request.param, exchange_conf, validate=True, load_leverage_tiers=True) + exchange_conf, validate=True, load_leverage_tiers=True) yield exchange, request.param @@ -162,8 +356,8 @@ class TestCCXTExchange(): 'stoploss': 'limit', }) - if exchangename == 'gateio': - # gateio doesn't have market orders on spot + if exchangename == 'gate': + # gate doesn't have market orders on spot return exch.validate_ordertypes({ 'entry': 'market', @@ -184,6 +378,32 @@ class TestCCXTExchange(): assert exchange.market_is_future(markets[pair]) + def test_ccxt_order_parse(self, exchange: EXCHANGE_FIXTURE_TYPE): + exch, exchange_name = exchange + if orders := EXCHANGES[exchange_name].get('sample_order'): + for order in orders: + po = exch._api.parse_order(order) + assert isinstance(po['id'], str) + assert po['id'] is not None + if len(order.keys()) < 5: + # Kucoin case + assert po['status'] == 'closed' + continue + assert po['timestamp'] == 1674493798550 + assert isinstance(po['datetime'], str) + assert isinstance(po['timestamp'], int) + assert isinstance(po['price'], float) + assert po['price'] == 15.5 + if po['average'] is not None: + assert isinstance(po['average'], float) + assert po['average'] == 15.5 + assert po['symbol'] == 'SOL/USDT' + assert isinstance(po['amount'], float) + assert po['amount'] == 1.1 + assert isinstance(po['status'], str) + else: + pytest.skip(f"No sample order available for exchange {exchange_name}") + def test_ccxt_fetch_tickers(self, exchange: EXCHANGE_FIXTURE_TYPE): exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] @@ -198,6 +418,25 @@ class TestCCXTExchange(): if EXCHANGES[exchangename].get('hasQuoteVolume'): assert tickers[pair]['quoteVolume'] is not None + def test_ccxt_fetch_tickers_futures(self, exchange_futures: EXCHANGE_FIXTURE_TYPE): + exch, exchangename = exchange_futures + if not exch or exchangename in ('gate'): + # exchange_futures only returns values for supported exchanges + return + + pair = EXCHANGES[exchangename]['pair'] + pair = EXCHANGES[exchangename].get('futures_pair', pair) + + tickers = exch.get_tickers() + assert pair in tickers + assert 'ask' in tickers[pair] + assert tickers[pair]['ask'] is not None + assert 'bid' in tickers[pair] + assert tickers[pair]['bid'] is not None + assert 'quoteVolume' in tickers[pair] + if EXCHANGES[exchangename].get('hasQuoteVolumeFutures'): + assert tickers[pair]['quoteVolume'] is not None + def test_ccxt_fetch_ticker(self, exchange: EXCHANGE_FIXTURE_TYPE): exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] @@ -221,10 +460,12 @@ class TestCCXTExchange(): assert len(l2['bids']) >= 1 l2_limit_range = exch._ft_has['l2_limit_range'] l2_limit_range_required = exch._ft_has['l2_limit_range_required'] - if exchangename == 'gateio': - # TODO: Gateio is unstable here at the moment, ignoring the limit partially. + if exchangename == 'gate': + # TODO: Gate is unstable here at the moment, ignoring the limit partially. return - for val in [1, 2, 5, 25, 100]: + for val in [1, 2, 5, 25, 50, 100]: + if val > 50 and exchangename == 'bybit': + continue l2 = exch.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: if val > 50: @@ -287,16 +528,21 @@ class TestCCXTExchange(): assert res[1] == timeframe assert res[2] == candle_type candles = res[3] - candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * 0.9 - candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms - assert len(candles) >= min(candle_count, candle_count1) + factor = 0.9 + candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor + candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor + assert len(candles) >= min(candle_count, candle_count1), \ + f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}" assert candles[0][0] == since_ms or (since_ms + timeframe_ms) def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): exc, exchangename = exchange - # For some weired reason, this test returns random lengths for bittrex. - if not exc._ft_has['ohlcv_has_history'] or exchangename in ('bittrex'): - return + if exchangename in ('bittrex'): + # For some weired reason, this test returns random lengths for bittrex. + pytest.skip("Exchange doesn't provide stable ohlcv history") + + if not exc._ft_has['ohlcv_has_history']: + pytest.skip("Exchange does not support candle history") pair = EXCHANGES[exchangename]['pair'] timeframe = EXCHANGES[exchangename]['timeframe'] self.ccxt__async_get_candle_history( @@ -476,23 +722,25 @@ class TestCCXTExchange(): ) liquidation_price = futures.dry_run_liquidation_price( - futures_pair, - 40000, - False, - 100, - 100, - 100, + pair=futures_pair, + open_rate=40000, + is_short=False, + amount=100, + stake_amount=100, + leverage=5, + wallet_balance=100, ) assert (isinstance(liquidation_price, float)) assert liquidation_price >= 0.0 liquidation_price = futures.dry_run_liquidation_price( - futures_pair, - 40000, - False, - 100, - 100, - 100, + pair=futures_pair, + open_rate=40000, + is_short=False, + amount=100, + stake_amount=100, + leverage=5, + wallet_balance=100, ) assert (isinstance(liquidation_price, float)) assert liquidation_price >= 0.0 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3714291d1..ef70c8ba1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5,29 +5,30 @@ from datetime import datetime, timedelta, timezone from random import randint from unittest.mock import MagicMock, Mock, PropertyMock, patch -import arrow import ccxt import pytest +from ccxt import DECIMAL_PLACES, ROUND, ROUND_UP, TICK_SIZE, TRUNCATE from pandas import DataFrame from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, - InvalidOrderException, OperationalException, PricingError, - TemporaryError) + InsufficientFundsError, InvalidOrderException, + OperationalException, PricingError, TemporaryError) from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision, date_minus_candles, market_is_active, price_to_precision, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, - calculate_backoff, remove_credentials) + calculate_backoff, remove_exchange_credentials) from freqtrade.exchange.exchange import amount_to_contract_precision from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import (generate_test_data_raw, get_mock_coro, get_patched_exchange, log_has, - log_has_re, num_log_has_re) +from freqtrade.util import dt_now, dt_ts +from tests.conftest import (EXMS, generate_test_data_raw, get_mock_coro, get_patched_exchange, + log_has, log_has_re, num_log_has_re) # Make sure to always keep one exchange here which is NOT subclassed!! -EXCHANGES = ['bittrex', 'binance', 'kraken', 'gateio'] +EXCHANGES = ['bittrex', 'binance', 'kraken', 'gate', 'kucoin', 'bybit'] get_entry_rate_data = [ ('other', 20, 19, 10, 0.0, 20), # Full ask side @@ -113,18 +114,21 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries + exchange.close() with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries + exchange.close() with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 + exchange.close() def test_init(default_conf, mocker, caplog): @@ -133,16 +137,14 @@ def test_init(default_conf, mocker, caplog): assert log_has('Instance is running with dry_run enabled', caplog) -def test_remove_credentials(default_conf, caplog) -> None: +def test_remove_exchange_credentials(default_conf) -> None: conf = deepcopy(default_conf) - conf['dry_run'] = False - remove_credentials(conf) + remove_exchange_credentials(conf['exchange'], False) assert conf['exchange']['key'] != '' assert conf['exchange']['secret'] != '' - conf['dry_run'] = True - remove_credentials(conf) + remove_exchange_credentials(conf['exchange'], True) assert conf['exchange']['key'] == '' assert conf['exchange']['secret'] == '' assert conf['exchange']['password'] == '' @@ -150,9 +152,9 @@ def test_remove_credentials(default_conf, caplog) -> None: def test_init_ccxt_kwargs(default_conf, mocker, caplog): - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - aei_mock = mocker.patch('freqtrade.exchange.Exchange.additional_exchange_init') + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_stakecurrency') + aei_mock = mocker.patch(f'{EXMS}.additional_exchange_init') caplog.set_level(logging.INFO) conf = copy.deepcopy(default_conf) @@ -218,33 +220,36 @@ def test_init_exception(default_conf, mocker): def test_exchange_resolver(default_conf, mocker, caplog): - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - - exchange = ExchangeResolver.load_exchange('zaif', default_conf) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=MagicMock())) + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + default_conf['exchange']['name'] = 'zaif' + exchange = ExchangeResolver.load_exchange(default_conf) assert isinstance(exchange, Exchange) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) caplog.clear() - exchange = ExchangeResolver.load_exchange('Bittrex', default_conf) + default_conf['exchange']['name'] = 'Bittrex' + exchange = ExchangeResolver.load_exchange(default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Bittrex) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) caplog.clear() - exchange = ExchangeResolver.load_exchange('kraken', default_conf) + default_conf['exchange']['name'] = 'kraken' + exchange = ExchangeResolver.load_exchange(default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) assert not isinstance(exchange, Binance) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) - exchange = ExchangeResolver.load_exchange('binance', default_conf) + default_conf['exchange']['name'] = 'binance' + exchange = ExchangeResolver.load_exchange(default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) @@ -253,7 +258,8 @@ def test_exchange_resolver(default_conf, mocker, caplog): caplog) # Test mapping - exchange = ExchangeResolver.load_exchange('binanceus', default_conf) + default_conf['exchange']['name'] = 'binanceus' + exchange = ExchangeResolver.load_exchange(default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) @@ -312,35 +318,54 @@ def test_amount_to_precision(amount, precision_mode, precision, expected,): assert amount_to_precision(amount, precision, precision_mode) == expected -@pytest.mark.parametrize("price,precision_mode,precision,expected", [ - (2.34559, 2, 4, 2.3456), - (2.34559, 2, 5, 2.34559), - (2.34559, 2, 3, 2.346), - (2.9999, 2, 3, 3.000), - (2.9909, 2, 3, 2.991), - # Tests for Tick_size - (2.34559, 4, 0.0001, 2.3456), - (2.34559, 4, 0.00001, 2.34559), - (2.34559, 4, 0.001, 2.346), - (2.9999, 4, 0.001, 3.000), - (2.9909, 4, 0.001, 2.991), - (2.9909, 4, 0.005, 2.995), - (2.9973, 4, 0.005, 3.0), - (2.9977, 4, 0.005, 3.0), - (234.43, 4, 0.5, 234.5), - (234.53, 4, 0.5, 235.0), - (0.891534, 4, 0.0001, 0.8916), - (64968.89, 4, 0.01, 64968.89), - (0.000000003483, 4, 1e-12, 0.000000003483), - +@pytest.mark.parametrize("price,precision_mode,precision,expected,rounding_mode", [ + # Tests for DECIMAL_PLACES, ROUND_UP + (2.34559, 2, 4, 2.3456, ROUND_UP), + (2.34559, 2, 5, 2.34559, ROUND_UP), + (2.34559, 2, 3, 2.346, ROUND_UP), + (2.9999, 2, 3, 3.000, ROUND_UP), + (2.9909, 2, 3, 2.991, ROUND_UP), + # Tests for DECIMAL_PLACES, ROUND + (2.345600000000001, DECIMAL_PLACES, 4, 2.3456, ROUND), + (2.345551, DECIMAL_PLACES, 4, 2.3456, ROUND), + (2.49, DECIMAL_PLACES, 0, 2., ROUND), + (2.51, DECIMAL_PLACES, 0, 3., ROUND), + (5.1, DECIMAL_PLACES, -1, 10., ROUND), + (4.9, DECIMAL_PLACES, -1, 0., ROUND), + # Tests for TICK_SIZE, ROUND_UP + (2.34559, TICK_SIZE, 0.0001, 2.3456, ROUND_UP), + (2.34559, TICK_SIZE, 0.00001, 2.34559, ROUND_UP), + (2.34559, TICK_SIZE, 0.001, 2.346, ROUND_UP), + (2.9999, TICK_SIZE, 0.001, 3.000, ROUND_UP), + (2.9909, TICK_SIZE, 0.001, 2.991, ROUND_UP), + (2.9909, TICK_SIZE, 0.005, 2.995, ROUND_UP), + (2.9973, TICK_SIZE, 0.005, 3.0, ROUND_UP), + (2.9977, TICK_SIZE, 0.005, 3.0, ROUND_UP), + (234.43, TICK_SIZE, 0.5, 234.5, ROUND_UP), + (234.53, TICK_SIZE, 0.5, 235.0, ROUND_UP), + (0.891534, TICK_SIZE, 0.0001, 0.8916, ROUND_UP), + (64968.89, TICK_SIZE, 0.01, 64968.89, ROUND_UP), + (0.000000003483, TICK_SIZE, 1e-12, 0.000000003483, ROUND_UP), + # Tests for TICK_SIZE, ROUND + (2.49, TICK_SIZE, 1., 2., ROUND), + (2.51, TICK_SIZE, 1., 3., ROUND), + (2.000000051, TICK_SIZE, 0.0000001, 2.0000001, ROUND), + (2.000000049, TICK_SIZE, 0.0000001, 2., ROUND), + (2.9909, TICK_SIZE, 0.005, 2.990, ROUND), + (2.9973, TICK_SIZE, 0.005, 2.995, ROUND), + (2.9977, TICK_SIZE, 0.005, 3.0, ROUND), + (234.24, TICK_SIZE, 0.5, 234., ROUND), + (234.26, TICK_SIZE, 0.5, 234.5, ROUND), + # Tests for TRUNCATTE + (2.34559, 2, 4, 2.3455, TRUNCATE), + (2.34559, 2, 5, 2.34559, TRUNCATE), + (2.34559, 2, 3, 2.345, TRUNCATE), + (2.9999, 2, 3, 2.999, TRUNCATE), + (2.9909, 2, 3, 2.990, TRUNCATE), ]) -def test_price_to_precision(price, precision_mode, precision, expected): - # digits counting mode - # DECIMAL_PLACES = 2 - # SIGNIFICANT_DIGITS = 3 - # TICK_SIZE = 4 - - assert price_to_precision(price, precision, precision_mode) == expected +def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode): + assert price_to_precision( + price, precision, precision_mode, rounding_mode=rounding_mode) == expected @pytest.mark.parametrize("price,precision_mode,precision,expected", [ @@ -362,9 +387,8 @@ def test_price_to_precision(price, precision_mode, precision, expected): def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precision, expected): markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}}) exchange = get_patched_exchange(mocker, default_conf, id="binance") - mocker.patch('freqtrade.exchange.Exchange.markets', markets) - mocker.patch('freqtrade.exchange.Exchange.precisionMode', - PropertyMock(return_value=precision_mode)) + mocker.patch(f'{EXMS}.markets', markets) + mocker.patch(f'{EXMS}.precisionMode', PropertyMock(return_value=precision_mode)) pair = 'ETH/BTC' assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected @@ -376,10 +400,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} # no pair found - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) with pytest.raises(ValueError, match=r'.*get market information.*'): exchange.get_min_pair_stake_amount('BNB/BTC', 1, stoploss) @@ -388,10 +409,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': None, 'max': None}, 'amount': {'min': None, 'max': None}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) assert result is None result = exchange.get_max_pair_stake_amount('ETH/BTC', 1) @@ -402,10 +420,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': 2, 'max': 10000}, 'amount': {'min': None, 'max': None}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) # min result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) expected_result = 2 * (1 + 0.05) / (1 - abs(stoploss)) @@ -422,12 +437,9 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': None, 'max': None}, 'amount': {'min': 2, 'max': 10000}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result = 2 * 2 * (1 + 0.05) / (1 - abs(stoploss)) + expected_result = 2 * 2 * (1 + 0.05) assert pytest.approx(result) == expected_result # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) @@ -436,17 +448,14 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) assert result == 20000 - # min amount and cost are set (cost is minimal) + # min amount and cost are set (cost is minimal and therefore ignored) markets["ETH/BTC"]["limits"] = { 'cost': {'min': 2, 'max': None}, 'amount': {'min': 2, 'max': None}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result = max(2, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss)) + expected_result = max(2, 2 * 2) * (1 + 0.05) assert pytest.approx(result) == expected_result # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) @@ -457,10 +466,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: 'cost': {'min': 8, 'max': 10000}, 'amount': {'min': 2, 'max': 500}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) expected_result = max(8, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss)) assert pytest.approx(result) == expected_result @@ -492,14 +498,14 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) assert result == 1000 + result = exchange.get_max_pair_stake_amount('ETH/BTC', 2, 12.0) + assert result == 1000 / 12 + markets["ETH/BTC"]["contractSize"] = '0.01' default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, id="binance") - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) # Contract size 0.01 result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) @@ -509,10 +515,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: assert result == 10 markets["ETH/BTC"]["contractSize"] = '10' - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) # With Leverage, Contract size 10 result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) assert pytest.approx(result) == (expected_result / 12) * 10.0 @@ -531,10 +534,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: 'cost': {'min': 0.0001, 'max': 4000}, 'amount': {'min': 0.001, 'max': 10000}, } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) expected_result = max(0.0001, 0.001 * 0.020405) * (1 + 0.05) / (1 - abs(stoploss)) assert round(result, 8) == round(expected_result, 8) @@ -592,12 +592,12 @@ def test_set_sandbox_exception(default_conf, mocker): def test__load_async_markets(default_conf, mocker, caplog): - mocker.patch('freqtrade.exchange.Exchange._init_ccxt') - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt') + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') exchange = Exchange(default_conf) exchange._api_async.load_markets = get_mock_coro(None) exchange._load_async_markets() @@ -614,19 +614,19 @@ def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) assert log_has('Unable to initialize markets.', caplog) expected_return = {'ETH/BTC': 'available'} api_mock = MagicMock() api_mock.load_markets = MagicMock(return_value=expected_return) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] ex = Exchange(default_conf) @@ -644,7 +644,7 @@ def test_reload_markets(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False) exchange._load_async_markets = MagicMock() - exchange._last_markets_refresh = arrow.utcnow().int_timestamp + exchange._last_markets_refresh = dt_ts() assert exchange.markets == initial_markets @@ -655,7 +655,7 @@ def test_reload_markets(default_conf, mocker, caplog): api_mock.load_markets = MagicMock(return_value=updated_markets) # more than 10 minutes have passed, reload is executed - exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60 + exchange._last_markets_refresh = dt_ts(dt_now() - timedelta(minutes=15)) exchange.reload_markets() assert exchange.markets == updated_markets assert exchange._load_async_markets.call_count == 1 @@ -684,11 +684,11 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) @@ -699,17 +699,17 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog): 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') with pytest.raises(OperationalException, match=r'XRP is not available as stake on .*' 'Available currencies are: BTC, ETH, USDT'): Exchange(default_conf) type(api_mock).load_markets = MagicMock(side_effect=ccxt.NetworkError('No connection.')) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) with pytest.raises(OperationalException, match=r'Could not load markets, therefore cannot start\. Please.*'): @@ -757,11 +757,11 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) @@ -770,10 +770,10 @@ def test_validate_pairs_not_available(default_conf, mocker): type(api_mock).markets = PropertyMock(return_value={ 'XRP/BTC': {'inactive': True, 'base': 'XRP', 'quote': 'BTC'} }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}._load_async_markets') with pytest.raises(OperationalException, match=r'not available'): Exchange(default_conf) @@ -782,19 +782,19 @@ def test_validate_pairs_not_available(default_conf, mocker): def test_validate_pairs_exception(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) + mocker.patch(f'{EXMS}.name', PropertyMock(return_value='Binance')) type(api_mock).markets = PropertyMock(return_value={}) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + mocker.patch(f'{EXMS}._init_ccxt', api_mock) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}._load_async_markets') with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): Exchange(default_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) Exchange(default_conf) assert log_has('Unable to validate pairs (assuming they are correct).', caplog) @@ -806,11 +806,11 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): 'XRP/BTC': {'quote': 'BTC', 'info': {'prohibitedIn': ['US']}}, 'NEO/BTC': {'quote': 'BTC', 'info': 'TestString'}, # info can also be a string ... }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.validate_stakecurrency') Exchange(default_conf) assert log_has("Pair XRP/BTC is restricted for some users on this exchange." @@ -825,11 +825,11 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'BTC'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) @@ -842,11 +842,11 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'BTC'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) assert type(api_mock).load_markets.call_count == 1 @@ -860,10 +860,10 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): 'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'}, 'HELLO-WORLD': {'quote': 'USDT'}, }) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_stakecurrency') with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"): Exchange(default_conf) @@ -883,11 +883,11 @@ def test_validate_timeframes(default_conf, mocker, timeframe): '1h': '1h'}) type(api_mock).timeframes = timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') Exchange(default_conf) @@ -903,9 +903,9 @@ def test_validate_timeframes_failed(default_conf, mocker): '1h': '1h'}) type(api_mock).timeframes = timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs', MagicMock()) with pytest.raises(OperationalException, match=r"Invalid timeframe '3m'. This exchange supports.*"): Exchange(default_conf) @@ -925,10 +925,10 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): # delete timeframes so magicmock does not autocreate it del api_mock.timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_stakecurrency') with pytest.raises(OperationalException, match=r'The ccxt library does not provide the list of timeframes ' r'for the exchange .* and this exchange ' @@ -945,11 +945,11 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): # delete timeframes so magicmock does not autocreate it del api_mock.timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={'timeframes': None})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pairs', MagicMock()) + mocker.patch(f'{EXMS}.validate_stakecurrency') with pytest.raises(OperationalException, match=r'The ccxt library does not provide the list of timeframes ' r'for the exchange .* and this exchange ' @@ -969,12 +969,12 @@ def test_validate_timeframes_not_in_config(default_conf, mocker): '1h': '1h'}) type(api_mock).timeframes = timeframes - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.validate_required_startup_candles') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.validate_required_startup_candles') Exchange(default_conf) @@ -985,26 +985,27 @@ def test_validate_pricing(default_conf, mocker): 'fetchTicker': True, } type(api_mock).has = PropertyMock(return_value=has) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.name', 'Binance') - ExchangeResolver.load_exchange('binance', default_conf) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_trading_mode_and_margin_mode') + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.name', 'Binance') + default_conf['exchange']['name'] = 'binance' + ExchangeResolver.load_exchange(default_conf) has.update({'fetchTicker': False}) with pytest.raises(OperationalException, match="Ticker pricing not available for .*"): - ExchangeResolver.load_exchange('binance', default_conf) + ExchangeResolver.load_exchange(default_conf) has.update({'fetchTicker': True}) default_conf['exit_pricing']['use_order_book'] = True - ExchangeResolver.load_exchange('binance', default_conf) + ExchangeResolver.load_exchange(default_conf) has.update({'fetchL2OrderBook': False}) with pytest.raises(OperationalException, match="Orderbook not available for .*"): - ExchangeResolver.load_exchange('binance', default_conf) + ExchangeResolver.load_exchange(default_conf) has.update({'fetchL2OrderBook': True}) @@ -1013,20 +1014,20 @@ def test_validate_pricing(default_conf, mocker): default_conf['margin_mode'] = MarginMode.ISOLATED with pytest.raises(OperationalException, match="Ticker pricing not available for .*"): - ExchangeResolver.load_exchange('binance', default_conf) + ExchangeResolver.load_exchange(default_conf) def test_validate_ordertypes(default_conf, mocker): api_mock = MagicMock() type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.name', 'Bittrex') default_conf['order_types'] = { 'entry': 'limit', @@ -1037,7 +1038,7 @@ def test_validate_ordertypes(default_conf, mocker): Exchange(default_conf) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) default_conf['order_types'] = { 'entry': 'limit', @@ -1060,14 +1061,56 @@ def test_validate_ordertypes(default_conf, mocker): Exchange(default_conf) +@pytest.mark.parametrize('exchange_name,stopadv, expected', [ + ('binance', 'last', True), + ('binance', 'mark', True), + ('binance', 'index', False), + ('bybit', 'last', True), + ('bybit', 'mark', True), + ('bybit', 'index', True), + ('okx', 'last', True), + ('okx', 'mark', True), + ('okx', 'index', True), + ('gate', 'last', True), + ('gate', 'mark', True), + ('gate', 'index', True), + ]) +def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name, stopadv, expected): + + api_mock = MagicMock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') + default_conf['order_types'] = { + 'entry': 'limit', + 'exit': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': True, + 'stoploss_price_type': stopadv, + } + default_conf['exchange']['name'] = exchange_name + if expected: + ExchangeResolver.load_exchange(default_conf) + else: + with pytest.raises(OperationalException, + match=r'On exchange stoploss price type is not supported for .*'): + ExchangeResolver.load_exchange(default_conf) + + def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.validate_stakecurrency') conf = copy.deepcopy(default_conf) Exchange(conf) @@ -1075,14 +1118,14 @@ def test_validate_order_types_not_in_config(default_conf, mocker): def test_validate_required_startup_candles(default_conf, mocker, caplog): api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) + mocker.patch(f'{EXMS}.name', PropertyMock(return_value='Binance')) - mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch(f'{EXMS}._init_ccxt', api_mock) + mocker.patch(f'{EXMS}.validate_timeframes') + mocker.patch(f'{EXMS}._load_async_markets') + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_pricing') + mocker.patch(f'{EXMS}.validate_stakecurrency') default_conf['startup_candle_count'] = 20 ex = Exchange(default_conf) @@ -1179,11 +1222,10 @@ def test_create_dry_run_order_fees( fee, ): mocker.patch( - 'freqtrade.exchange.Exchange.get_fee', - side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 + f'{EXMS}.get_fee', + side_effect=lambda symbol, taker_or_maker: 2.0 if taker_or_maker == 'taker' else 1.0 ) - mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', - return_value=price_side == 'other') + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=price_side == 'other') exchange = get_patched_exchange(mocker, default_conf) order = exchange.create_dry_run_order( @@ -1200,47 +1242,56 @@ def test_create_dry_run_order_fees( else: assert order['fee'] is None - mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', - return_value=price_side != 'other') + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=price_side != 'other') order1 = exchange.fetch_dry_run_order(order['id']) assert order1['fee']['rate'] == fee -@pytest.mark.parametrize("side,price,filled", [ +@pytest.mark.parametrize("side,price,filled,converted", [ # order_book_l2_usd spread: # best ask: 25.566 # best bid: 25.563 - ("buy", 25.563, False), - ("buy", 25.566, True), - ("sell", 25.566, False), - ("sell", 25.563, True), + ("buy", 25.563, False, False), + ("buy", 25.566, True, False), + ("sell", 25.566, False, False), + ("sell", 25.563, True, False), + ("buy", 29.563, True, True), + ("sell", 21.563, True, True), ]) +@pytest.mark.parametrize("leverage", [1, 2, 5]) @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled, - exchange_name, order_book_l2_usd): +def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled, caplog, + exchange_name, order_book_l2_usd, converted, leverage): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, exchange_has=MagicMock(return_value=True), fetch_l2_order_book=order_book_l2_usd, ) - order = exchange.create_dry_run_order( + order = exchange.create_order( pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=price, - leverage=1.0 + leverage=leverage, ) assert order_book_l2_usd.call_count == 1 assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side - assert order["type"] == "limit" + if not converted: + assert order["average"] == price + assert order["type"] == "limit" + else: + # Converted to market order + assert order["type"] == "market" + assert 25.5 < order["average"] < 25.6 + assert log_has_re(r"Converted .* to market order.*", caplog) + assert order["symbol"] == "LTC/USDT" - assert order["average"] == price assert order['status'] == 'open' if not filled else 'closed' order_book_l2_usd.reset_mock() @@ -1249,12 +1300,12 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill assert order_book_l2_usd.call_count == (1 if not filled else 0) assert order_closed['status'] == ('open' if not filled else 'closed') assert order_closed['filled'] == (0 if not filled else 1) + assert order_closed['cost'] == 1 * order_closed['average'] order_book_l2_usd.reset_mock() # Empty orderbook test - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', - return_value={'asks': [], 'bids': []}) + mocker.patch(f'{EXMS}.fetch_l2_order_book', return_value={'asks': [], 'bids': []}) exchange._dry_run_open_orders[order['id']]['status'] = 'open' order_closed = exchange.fetch_dry_run_order(order['id']) @@ -1272,23 +1323,24 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill ("sell", 25.564, 1000, 25.5555), # More than orderbook return ("sell", 27, 10000, 25.65), # max-slippage 5% ]) +@pytest.mark.parametrize("leverage", [1, 2, 5]) @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amount, endprice, - exchange_name, order_book_l2_usd): + exchange_name, order_book_l2_usd, leverage): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, exchange_has=MagicMock(return_value=True), fetch_l2_order_book=order_book_l2_usd, ) - order = exchange.create_dry_run_order( + order = exchange.create_order( pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate, - leverage=1.0 + leverage=leverage, ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] @@ -1297,6 +1349,8 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou assert order["symbol"] == "LTC/USDT" assert order['status'] == 'closed' assert order['filled'] == amount + assert order['amount'] == amount + assert pytest.approx(order['cost']) == amount * order['average'] assert round(order["average"], 4) == round(endprice, 4) @@ -1322,8 +1376,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, }) default_conf['dry_run'] = False default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange._set_leverage = MagicMock() exchange.set_margin_mode = MagicMock() @@ -1375,9 +1429,10 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert order['amount'] == 0.01 -def test_buy_dry_run(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_buy_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) order = exchange.create_order(pair='ETH/BTC', ordertype='limit', side="buy", amount=1, rate=200, leverage=1.0, @@ -1401,8 +1456,8 @@ def test_buy_prod(default_conf, mocker, exchange_name): } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", @@ -1416,7 +1471,10 @@ def test_buy_prod(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'buy' assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] is None + if exchange._order_needs_price(order_type): + assert api_mock.create_order.call_args[0][4] == 200 + else: + assert api_mock.create_order.call_args[0][4] is None api_mock.create_order.reset_mock() order_type = 'limit' @@ -1485,8 +1543,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order_type = 'limit' @@ -1521,7 +1579,10 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'buy' assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] is None + if exchange._order_needs_price(order_type): + assert api_mock.create_order.call_args[0][4] == 200 + else: + assert api_mock.create_order.call_args[0][4] is None # Market orders should not send timeInForce!! assert "timeInForce" not in api_mock.create_order.call_args[0][5] @@ -1551,8 +1612,8 @@ def test_sell_prod(default_conf, mocker, exchange_name): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, @@ -1565,7 +1626,10 @@ def test_sell_prod(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'sell' assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] is None + if exchange._order_needs_price(order_type): + assert api_mock.create_order.call_args[0][4] == 200 + else: + assert api_mock.create_order.call_args[0][4] is None api_mock.create_order.reset_mock() order_type = 'limit' @@ -1579,13 +1643,13 @@ def test_sell_prod(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][4] == 200 # test exception handling - with pytest.raises(DependencyException): + with pytest.raises(InsufficientFundsError): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200, leverage=1.0) - with pytest.raises(DependencyException): + with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200, @@ -1624,8 +1688,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): }) api_mock.options = {} default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order_type = 'limit' @@ -1659,7 +1723,10 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'sell' assert api_mock.create_order.call_args[0][3] == 1 - assert api_mock.create_order.call_args[0][4] is None + if exchange._order_needs_price(order_type): + assert api_mock.create_order.call_args[0][4] == 200 + else: + assert api_mock.create_order.call_args[0][4] is None # Market orders should not send timeInForce!! assert "timeInForce" not in api_mock.create_order.call_args[0][5] @@ -1691,7 +1758,7 @@ def test_get_balances_prod(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_positions(default_conf, mocker, exchange_name): - mocker.patch('freqtrade.exchange.Exchange.validate_trading_mode_and_margin_mode') + mocker.patch(f'{EXMS}.validate_trading_mode_and_margin_mode') api_mock = MagicMock() api_mock.fetch_positions = MagicMock(return_value=[ {'symbol': 'ETH/USDT:USDT', 'leverage': 5}, @@ -1710,6 +1777,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 = { @@ -1742,12 +1874,12 @@ def test_fetch_trading_fees(default_conf, mocker): 'maker': 0.0, 'taker': 0.0005} } - exchange_name = 'gateio' + exchange_name = 'gate' default_conf['dry_run'] = False default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED api_mock.fetch_trading_fees = MagicMock(return_value=tick) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert '1INCH/USDT:USDT' in exchange._trading_fees @@ -1762,7 +1894,7 @@ def test_fetch_trading_fees(default_conf, mocker): api_mock.fetch_trading_fees = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.fetch_trading_fees() - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) assert exchange.fetch_trading_fees() == {} @@ -1782,7 +1914,7 @@ def test_fetch_bids_asks(default_conf, mocker): } exchange_name = 'binance' api_mock.fetch_bids_asks = MagicMock(return_value=tick) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker bidsasks = exchange.fetch_bids_asks() @@ -1815,7 +1947,7 @@ def test_fetch_bids_asks(default_conf, mocker): api_mock.fetch_bids_asks = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.fetch_bids_asks() - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) assert exchange.fetch_bids_asks() == {} @@ -1834,7 +1966,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): 'last': 41, } } - mocker.patch('freqtrade.exchange.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) api_mock.fetch_tickers = MagicMock(return_value=tick) api_mock.fetch_bids_asks = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) @@ -1877,7 +2009,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): api_mock.fetch_bids_asks.reset_mock() default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() @@ -1886,7 +2018,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): api_mock.fetch_tickers.reset_mock() api_mock.fetch_bids_asks.reset_mock() - mocker.patch('freqtrade.exchange.exchange.Exchange.exchange_has', return_value=False) + mocker.patch(f'{EXMS}.exchange_has', return_value=False) assert exchange.get_tickers() == {} @@ -1944,7 +2076,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_ exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) ohlcv = [ [ - arrow.utcnow().int_timestamp * 1000, # unix timestamp ms + dt_ts(), # unix timestamp ms 1, # open 2, # high 3, # low @@ -1964,7 +2096,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_ ret = exchange.get_historic_ohlcv( pair, "5m", - int((arrow.utcnow().int_timestamp - since) * 1000), + dt_ts(dt_now() - timedelta(seconds=since)), candle_type=candle_type ) @@ -1982,7 +2114,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_ ret = exchange.get_historic_ohlcv( pair, "5m", - int((arrow.utcnow().int_timestamp - since) * 1000), + dt_ts(dt_now() - timedelta(seconds=since)), candle_type=candle_type ) assert log_has_re(r"Async code raised an exception: .*", caplog) @@ -2034,7 +2166,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None: ohlcv = [ [ - (arrow.utcnow().shift(minutes=-5).int_timestamp) * 1000, # unix timestamp ms + dt_ts(dt_now() - timedelta(minutes=5)), # unix timestamp ms 1, # open 2, # high 3, # low @@ -2042,7 +2174,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog, candle_type) -> None 5, # volume (in quote currency) ], [ - arrow.utcnow().int_timestamp * 1000, # unix timestamp ms + dt_ts(), # unix timestamp ms 3, # open 1, # high 4, # low @@ -2140,7 +2272,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach time_machine.move_to(start + timedelta(hours=99, minutes=30)) exchange = get_patched_exchange(mocker, default_conf) - mocker.patch("freqtrade.exchange.Exchange.ohlcv_candle_limit", return_value=100) + mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100) assert exchange._startup_candle_count == 0 exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) @@ -2165,7 +2297,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert len(res[pair1]) == 99 assert len(res[pair2]) == 99 assert exchange._klines - assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-2][0] // 1000 exchange._api_async.fetch_ohlcv.reset_mock() # Returned from cache @@ -2174,7 +2306,7 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert len(res) == 2 assert len(res[pair1]) == 99 assert len(res[pair2]) == 99 - assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-2][0] // 1000 # Move time 1 candle further but result didn't change yet time_machine.move_to(start + timedelta(hours=101)) @@ -2184,13 +2316,13 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert len(res[pair1]) == 99 assert len(res[pair2]) == 99 assert res[pair2].at[0, 'open'] - assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-2][0] // 1000 refresh_pior = exchange._pairs_last_refresh_time[pair1] # New candle on exchange - return 100 candles - but skip one candle so we actually get 2 candles # in one go new_startdate = (start + timedelta(hours=2)).strftime('%Y-%m-%d %H:%M') - # mocker.patch("freqtrade.exchange.Exchange.ohlcv_candle_limit", return_value=100) + # mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100) ohlcv = generate_test_data_raw('1h', 100, new_startdate) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) res = exchange.refresh_latest_ohlcv(pairs) @@ -2202,8 +2334,8 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert res[pair2].at[0, 'open'] assert refresh_pior != exchange._pairs_last_refresh_time[pair1] - assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-1][0] // 1000 - assert exchange._pairs_last_refresh_time[pair2] == ohlcv[-1][0] // 1000 + assert exchange._pairs_last_refresh_time[pair1] == ohlcv[-2][0] // 1000 + assert exchange._pairs_last_refresh_time[pair2] == ohlcv[-2][0] // 1000 exchange._api_async.fetch_ohlcv.reset_mock() # Retry same call - from cache @@ -2228,12 +2360,11 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert res[pair2].at[0, 'open'] -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): ohlcv = [ [ - arrow.utcnow().int_timestamp * 1000, # unix timestamp ms + dt_ts(), # unix timestamp ms 1, # open 2, # high 3, # low @@ -2257,7 +2388,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ assert res[3] == ohlcv assert exchange._api_async.fetch_ohlcv.call_count == 1 assert not log_has(f"Using cached candle (OHLCV) data for {pair} ...", caplog) - + exchange.close() # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_get_candle_history", "fetch_ohlcv", @@ -2270,17 +2401,19 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT, - (arrow.utcnow().int_timestamp - 2000) * 1000) + dt_ts(dt_now() - timedelta(seconds=2000))) + + exchange.close() with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical candle \(OHLCV\) data\..*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", CandleType.SPOT, - (arrow.utcnow().int_timestamp - 2000) * 1000) + dt_ts(dt_now() - timedelta(seconds=2000))) + exchange.close() -@pytest.mark.asyncio async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): from freqtrade.exchange.common import _reset_logging_mixin _reset_logging_mixin() @@ -2290,8 +2423,8 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): "kucoin GET https://openapi-v2.kucoin.com/api/v1/market/candles?" "symbol=ETH-BTC&type=5min&startAt=1640268735&endAt=1640418735" "429 Too Many Requests" '{"code":"429000","msg":"Too Many Requests"}')) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="KuCoin") - mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='KuCoin')) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kucoin") + mocker.patch(f'{EXMS}.name', PropertyMock(return_value='KuCoin')) msg = "Kucoin 429 error, avoid triggering DDosProtection backoff delay" assert not num_log_has_re(msg, caplog) @@ -2300,7 +2433,7 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): with pytest.raises(DDosProtection, match=r'429 Too Many Requests'): await exchange._async_get_candle_history( "ETH/BTC", "5m", CandleType.SPOT, - since_ms=(arrow.utcnow().int_timestamp - 2000) * 1000, count=3) + since_ms=dt_ts(dt_now() - timedelta(seconds=2000)), count=3) assert num_log_has_re(msg, caplog) == 3 caplog.clear() @@ -2317,13 +2450,13 @@ async def test__async_kucoin_get_candle_history(default_conf, mocker, caplog): with pytest.raises(DDosProtection, match=r'429 Too Many Requests'): await exchange._async_get_candle_history( "ETH/BTC", "5m", CandleType.SPOT, - (arrow.utcnow().int_timestamp - 2000) * 1000, count=3) + dt_ts(dt_now() - timedelta(seconds=2000)), count=3) # Expect the "returned exception" message 12 times (4 retries * 3 (loop)) assert num_log_has_re(msg, caplog) == 12 assert num_log_has_re(msg2, caplog) == 9 + exchange.close() -@pytest.mark.asyncio async def test__async_get_candle_history_empty(default_conf, mocker, caplog): """ Test empty exchange result """ ohlcv = [] @@ -2343,6 +2476,7 @@ async def test__async_get_candle_history_empty(default_conf, mocker, caplog): assert res[2] == CandleType.SPOT assert res[3] == ohlcv assert exchange._api_async.fetch_ohlcv.call_count == 1 + exchange.close() def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): @@ -2449,8 +2583,7 @@ def test_get_entry_rate(mocker, default_conf, caplog, side, ask, bid, default_conf['entry_pricing']['price_last_balance'] = last_ab default_conf['entry_pricing']['price_side'] = side exchange = get_patched_exchange(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': ask, 'last': last, 'bid': bid}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'last': last, 'bid': bid}) assert exchange.get_rate('ETH/BTC', side="entry", is_short=False, refresh=True) == expected assert not log_has("Using cached entry rate for ETH/BTC.", caplog) @@ -2471,8 +2604,7 @@ def test_get_exit_rate(default_conf, mocker, caplog, side, bid, ask, default_conf['exit_pricing']['price_side'] = side if last_ab is not None: default_conf['exit_pricing']['price_last_balance'] = last_ab - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': ask, 'bid': bid, 'last': last}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'bid': bid, 'last': last}) pair = "ETH/BTC" # Test regular mode @@ -2505,8 +2637,7 @@ def test_get_ticker_rate_error(mocker, entry, default_conf, caplog, side, is_sho default_conf['exit_pricing']['price_side'] = side default_conf['exit_pricing']['price_last_balance'] = last_ab exchange = get_patched_exchange(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': ask, 'last': last, 'bid': bid}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': ask, 'last': last, 'bid': bid}) with pytest.raises(PricingError): exchange.get_rate('ETH/BTC', refresh=True, side=entry, is_short=is_short) @@ -2530,7 +2661,7 @@ def test_get_exit_rate_orderbook( default_conf['exit_pricing']['use_order_book'] = True default_conf['exit_pricing']['order_book_top'] = 1 pair = "ETH/BTC" - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2) + mocker.patch(f'{EXMS}.fetch_l2_order_book', order_book_l2) exchange = get_patched_exchange(mocker, default_conf) rate = exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) assert not log_has("Using cached exit rate for ETH/BTC.", caplog) @@ -2548,8 +2679,7 @@ def test_get_exit_rate_orderbook_exception(default_conf, mocker, caplog): default_conf['exit_pricing']['order_book_top'] = 1 pair = "ETH/BTC" # Test What happens if the exchange returns an empty orderbook. - mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', - return_value={'bids': [[]], 'asks': [[]]}) + mocker.patch(f'{EXMS}.fetch_l2_order_book', return_value={'bids': [[]], 'asks': [[]]}) exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError): exchange.get_rate(pair, refresh=True, side="exit", is_short=False) @@ -2563,8 +2693,7 @@ def test_get_exit_rate_exception(default_conf, mocker, is_short): # Ticker on one side can be empty in certain circumstances. default_conf['exit_pricing']['price_side'] = 'ask' pair = "ETH/BTC" - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': None, 'bid': 0.12, 'last': None}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': None, 'bid': 0.12, 'last': None}) exchange = get_patched_exchange(mocker, default_conf) with pytest.raises(PricingError, match=r"Exit-Rate for ETH/BTC was empty."): exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) @@ -2572,8 +2701,7 @@ def test_get_exit_rate_exception(default_conf, mocker, is_short): exchange._config['exit_pricing']['price_side'] = 'bid' assert exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) == 0.12 # Reverse sides - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - return_value={'ask': 0.13, 'bid': None, 'last': None}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'ask': 0.13, 'bid': None, 'last': None}) with pytest.raises(PricingError, match=r"Exit-Rate for ETH/BTC was empty."): exchange.get_rate(pair, refresh=True, side="exit", is_short=is_short) @@ -2743,7 +2871,6 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na assert res_ohlcv[9][5] == 2.31452783 -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, fetch_trades_result): @@ -2771,8 +2898,8 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, assert exchange._api_async.fetch_trades.call_args[1]['limit'] == 1000 assert exchange._api_async.fetch_trades.call_args[1]['params'] == {'from': '123'} assert log_has_re(f"Fetching trades for pair {pair}, params: .*", caplog) + exchange.close() - exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_fetch_trades", "fetch_trades", pair='ABCD/BTC', since=None) @@ -2781,16 +2908,17 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, with pytest.raises(OperationalException, match=r'Could not fetch trade data*'): api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) + await exchange._async_fetch_trades(pair, since=dt_ts(dt_now() - timedelta(seconds=2000))) + exchange.close() with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical trade data\..*'): api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) + await exchange._async_fetch_trades(pair, since=dt_ts(dt_now() - timedelta(seconds=2000))) + exchange.close() -@pytest.mark.asyncio @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, exchange_name, fetch_trades_result): @@ -2825,6 +2953,7 @@ async def test__async_fetch_trades_contract_size(default_conf, mocker, caplog, e pair = 'ETH/USDT:USDT' res = await exchange._async_fetch_trades(pair, since=None, params=None) assert res[0][5] == 300 + exchange.close() @pytest.mark.asyncio @@ -2939,7 +3068,7 @@ async def test__async_get_trade_history_time_empty(default_conf, mocker, caplog, @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades_history): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) pair = 'ETH/BTC' @@ -2961,7 +3090,7 @@ def test_get_historic_trades(default_conf, mocker, caplog, exchange_name, trades @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange_name, trades_history): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=False) + mocker.patch(f'{EXMS}.exchange_has', return_value=False) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) pair = 'ETH/BTC' @@ -2977,7 +3106,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange def test_cancel_order_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} @@ -3105,33 +3234,33 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', return_value={'for': 123}) - mocker.patch('freqtrade.exchange.Gateio.fetch_stoploss_order', return_value={'for': 123}) + mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value={'for': 123}) + mocker.patch('freqtrade.exchange.gate.Gate.fetch_stoploss_order', return_value={'for': 123}) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) res = {'fee': {}, 'status': 'canceled', 'amount': 1234} - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value=res) - mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', return_value=res) + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=res) + mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', return_value=res) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co == res - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', return_value='canceled') - mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', return_value='canceled') + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value='canceled') + mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', return_value='canceled') # Fall back to fetch_stoploss_order co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co == {'for': 123} exc = InvalidOrderException("") - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=exc) - mocker.patch('freqtrade.exchange.Gateio.fetch_stoploss_order', side_effect=exc) + mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=exc) + mocker.patch('freqtrade.exchange.gate.Gate.fetch_stoploss_order', side_effect=exc) co = exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=555) assert co['amount'] == 555 assert co == {'fee': {}, 'status': 'canceled', 'amount': 555, 'info': {}} with pytest.raises(InvalidOrderException): exc = InvalidOrderException("Did not find order") - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=exc) - mocker.patch('freqtrade.exchange.Gateio.cancel_stoploss_order', side_effect=exc) + mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=exc) + mocker.patch('freqtrade.exchange.gate.Gate.cancel_stoploss_order', side_effect=exc) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange.cancel_stoploss_order_with_result(order_id='_', pair='TKN/BTC', amount=123) @@ -3224,7 +3353,7 @@ def test_fetch_order_or_stoploss_order(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='binance') fetch_order_mock = MagicMock() fetch_stoploss_order_mock = MagicMock() - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, fetch_order=fetch_order_mock, fetch_stoploss_order=fetch_stoploss_order_mock, ) @@ -3264,7 +3393,7 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, default_conf["dry_run"] = False default_conf["trading_mode"] = trading_mode default_conf["margin_mode"] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) api_mock = MagicMock() api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV', @@ -3307,7 +3436,7 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name, trading_mode, 'get_trades_for_order', 'fetch_my_trades', order_id=order_id, pair='ETH/USDT:USDT', since=since) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=False)) assert exchange.get_trades_for_order(order_id, 'ETH/USDT:USDT', since) == [] @@ -3339,7 +3468,7 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='bittrex') with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss( + exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, @@ -3353,7 +3482,7 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, _init_ccxt=MagicMock(return_value=MagicMock()), _load_async_markets=MagicMock(), validate_pairs=MagicMock(), @@ -3373,7 +3502,7 @@ def test_merge_ft_has_dict(default_conf, mocker): ex = Binance(default_conf) assert ex._ft_has != Exchange._ft_has_default assert ex.get_option('stoploss_on_exchange') - assert ex.get_option('order_time_in_force') == ['GTC', 'FOK', 'IOC'] + assert ex.get_option('order_time_in_force') == ['GTC', 'FOK', 'IOC', 'PO'] assert ex.get_option('trades_pagination') == 'id' assert ex.get_option('trades_pagination_arg') == 'fromId' @@ -3388,7 +3517,7 @@ def test_merge_ft_has_dict(default_conf, mocker): def test_get_valid_pair_combination(default_conf, mocker, markets): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, _init_ccxt=MagicMock(return_value=MagicMock()), _load_async_markets=MagicMock(), validate_pairs=MagicMock(), @@ -3480,7 +3609,7 @@ def test_get_markets(default_conf, mocker, markets_static, spot_only, futures_only, expected_keys, test_comment # Here for debugging purposes (Not used within method) ): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, _init_ccxt=MagicMock(return_value=MagicMock()), _load_async_markets=MagicMock(), validate_pairs=MagicMock(), @@ -3499,7 +3628,7 @@ def test_get_markets(default_conf, mocker, markets_static, def test_get_markets_error(default_conf, mocker): ex = get_patched_exchange(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=None)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=None)) with pytest.raises(OperationalException, match="Markets were not loaded."): ex.get_markets('LTC', 'USDT', True, False) @@ -3644,7 +3773,7 @@ def test_market_is_tradable( quote, spot, margin, futures, trademode, add_dict, exchange, expected_result ) -> None: default_conf['trading_mode'] = trademode - mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') + mocker.patch(f'{EXMS}.validate_trading_mode_and_margin_mode') ex = get_patched_exchange(mocker, default_conf, id=exchange) market = { 'symbol': market_symbol, @@ -3689,7 +3818,7 @@ def test_order_has_fee(order, expected) -> None: (0.34, 'USDT', 0.01)), ]) def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None: - mocker.patch('freqtrade.exchange.Exchange.calculate_fee_rate', MagicMock(return_value=0.01)) + mocker.patch(f'{EXMS}.calculate_fee_rate', MagicMock(return_value=0.01)) ex = get_patched_exchange(mocker, default_conf) assert ex.extract_cost_curr_rate(order['fee'], order['symbol'], cost=20, amount=1) == expected @@ -3734,7 +3863,7 @@ def test_extract_cost_curr_rate(mocker, default_conf, order, expected) -> None: 'fee': {'currency': None, 'cost': 0.005}}, None, None), ]) def test_calculate_fee_rate(mocker, default_conf, order, expected, unknown_fee_rate) -> None: - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 0.081}) + mocker.patch(f'{EXMS}.fetch_ticker', return_value={'last': 0.081}) if unknown_fee_rate: default_conf['exchange']['unknown_fee_rate'] = unknown_fee_rate @@ -3806,7 +3935,7 @@ def test__get_funding_fees_from_exchange(default_conf, mocker, exchange_name): ]) type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) - # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + # mocker.patch(f'{EXMS}.get_funding_fees', lambda pair, since: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') unix_time = int(date_time.timestamp()) @@ -3854,29 +3983,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) @@ -3911,14 +4017,14 @@ def test_set_margin_mode(mocker, default_conf, margin_mode): ("bittrex", TradingMode.MARGIN, MarginMode.ISOLATED, True), ("bittrex", TradingMode.FUTURES, MarginMode.CROSS, True), ("bittrex", TradingMode.FUTURES, MarginMode.ISOLATED, True), - ("gateio", TradingMode.MARGIN, MarginMode.ISOLATED, True), + ("gate", TradingMode.MARGIN, MarginMode.ISOLATED, True), ("okx", TradingMode.SPOT, None, False), ("okx", TradingMode.MARGIN, MarginMode.CROSS, True), ("okx", TradingMode.MARGIN, MarginMode.ISOLATED, True), ("okx", TradingMode.FUTURES, MarginMode.CROSS, True), ("binance", TradingMode.FUTURES, MarginMode.ISOLATED, False), - ("gateio", TradingMode.FUTURES, MarginMode.ISOLATED, False), + ("gate", TradingMode.FUTURES, MarginMode.ISOLATED, False), ("okx", TradingMode.FUTURES, MarginMode.ISOLATED, False), # * Remove once implemented @@ -3926,16 +4032,16 @@ def test_set_margin_mode(mocker, default_conf, margin_mode): ("binance", TradingMode.FUTURES, MarginMode.CROSS, True), ("kraken", TradingMode.MARGIN, MarginMode.CROSS, True), ("kraken", TradingMode.FUTURES, MarginMode.CROSS, True), - ("gateio", TradingMode.MARGIN, MarginMode.CROSS, True), - ("gateio", TradingMode.FUTURES, MarginMode.CROSS, True), + ("gate", TradingMode.MARGIN, MarginMode.CROSS, True), + ("gate", TradingMode.FUTURES, MarginMode.CROSS, True), # * Uncomment once implemented # ("binance", TradingMode.MARGIN, MarginMode.CROSS, False), # ("binance", TradingMode.FUTURES, MarginMode.CROSS, False), # ("kraken", TradingMode.MARGIN, MarginMode.CROSS, False), # ("kraken", TradingMode.FUTURES, MarginMode.CROSS, False), - # ("gateio", TradingMode.MARGIN, MarginMode.CROSS, False), - # ("gateio", TradingMode.FUTURES, MarginMode.CROSS, False), + # ("gate", TradingMode.MARGIN, MarginMode.CROSS, False), + # ("gate", TradingMode.FUTURES, MarginMode.CROSS, False), ]) def test_validate_trading_mode_and_margin_mode( default_conf, @@ -3957,10 +4063,10 @@ def test_validate_trading_mode_and_margin_mode( @pytest.mark.parametrize("exchange_name,trading_mode,ccxt_config", [ ("binance", "spot", {}), ("binance", "margin", {"options": {"defaultType": "margin"}}), - ("binance", "futures", {"options": {"defaultType": "future"}}), + ("binance", "futures", {"options": {"defaultType": "swap"}}), ("bybit", "spot", {"options": {"defaultType": "spot"}}), - ("bybit", "futures", {"options": {"defaultType": "linear"}}), - ("gateio", "futures", {"options": {"defaultType": "swap"}}), + ("bybit", "futures", {"options": {"defaultType": "swap"}}), + ("gate", "futures", {"options": {"defaultType": "swap"}}), ("hitbtc", "futures", {"options": {"defaultType": "swap"}}), ("kraken", "futures", {"options": {"defaultType": "swap"}}), ("kucoin", "futures", {"options": {"defaultType": "swap"}}), @@ -3991,7 +4097,7 @@ def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value, default_conf['margin_mode'] = 'isolated' api_mock = MagicMock() type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False}) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gate") assert exchange.get_max_leverage(pair, nominal_value) == max_lev @@ -4136,10 +4242,10 @@ def test_combine_funding_and_mark( # ('kraken', "2021-09-01 00:00:00", "2021-09-01 07:59:59", 30.0, -0.0012443999999999999), # ('kraken', "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, 0.0045759), # ('kraken', "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0008289), - ('gateio', 0, 2, "2021-09-01 00:10:00", "2021-09-01 04:00:00", 30.0, 0.0), - ('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999), - ('gateio', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999), - ('gateio', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), + ('gate', 0, 2, "2021-09-01 00:10:00", "2021-09-01 04:00:00", 30.0, 0.0), + ('gate', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999), + ('gate', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999), + ('gate', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235), # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895), @@ -4197,7 +4303,7 @@ def test__fetch_and_calculate_funding_fees( d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z') funding_rate_history = { 'binance': funding_rate_history_octohourly, - 'gateio': funding_rate_history_octohourly, + 'gate': funding_rate_history_octohourly, }[exchange][rate_start:rate_end] api_mock = MagicMock() api_mock.fetch_funding_rate_history = get_mock_coro(return_value=funding_rate_history) @@ -4206,8 +4312,7 @@ def test__fetch_and_calculate_funding_fees( type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) - mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock( - return_value=['1h', '4h', '8h'])) + mocker.patch(f'{EXMS}.timeframes', PropertyMock(return_value=['1h', '4h', '8h'])) funding_fees = ex._fetch_and_calculate_funding_fees( pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == expected_fees @@ -4217,7 +4322,7 @@ def test__fetch_and_calculate_funding_fees( assert pytest.approx(funding_fees) == -expected_fees # Return empty "refresh_latest" - mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", return_value={}) + mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value={}) ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) with pytest.raises(ExchangeError, match="Could not find funding rates."): ex._fetch_and_calculate_funding_fees( @@ -4226,7 +4331,7 @@ def test__fetch_and_calculate_funding_fees( @pytest.mark.parametrize('exchange,expected_fees', [ ('binance', -0.0009140999999999999), - ('gateio', -0.0009140999999999999), + ('gate', -0.0009140999999999999), ]) def test__fetch_and_calculate_funding_fees_datetime_called( mocker, @@ -4243,7 +4348,7 @@ def test__fetch_and_calculate_funding_fees_datetime_called( return_value=funding_rate_history_octohourly) type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True}) type(api_mock).has = PropertyMock(return_value={'fetchFundingRateHistory': True}) - mocker.patch('freqtrade.exchange.Exchange.timeframes', PropertyMock(return_value=['4h', '8h'])) + mocker.patch(f'{EXMS}.timeframes', PropertyMock(return_value=['4h', '8h'])) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) d1 = datetime.strptime("2021-09-01 00:00:00 +0000", '%Y-%m-%d %H:%M:%S %z') @@ -4266,7 +4371,7 @@ def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_m default_conf['trading_mode'] = trading_mode default_conf['margin_mode'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, api_mock) - mocker.patch('freqtrade.exchange.Exchange.markets', { + mocker.patch(f'{EXMS}.markets', { 'LTC/USD': { 'symbol': 'LTC/USD', 'contractSize': None, @@ -4302,7 +4407,7 @@ def test__order_contracts_to_amount( api_mock = MagicMock() default_conf['trading_mode'] = trading_mode default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch(f'{EXMS}.markets', markets) exchange = get_patched_exchange(mocker, default_conf, api_mock) orders = [ @@ -4367,7 +4472,7 @@ def test__order_contracts_to_amount( 'info': {}, }, { - # Realistic stoploss order on gateio. + # Realistic stoploss order on gate. 'id': '123456380', 'clientOrderId': '12345638203', 'timestamp': None, @@ -4424,7 +4529,7 @@ def test__trades_contracts_to_amount( api_mock = MagicMock() default_conf['trading_mode'] = trading_mode default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch(f'{EXMS}.markets', markets) exchange = get_patched_exchange(mocker, default_conf, api_mock) trades = [ @@ -4460,7 +4565,7 @@ def test__amount_to_contracts( default_conf['trading_mode'] = 'spot' default_conf['margin_mode'] = 'isolated' exchange = get_patched_exchange(mocker, default_conf, api_mock) - mocker.patch('freqtrade.exchange.Exchange.markets', { + mocker.patch(f'{EXMS}.markets', { 'LTC/USD': { 'symbol': 'LTC/USD', 'contractSize': None, @@ -4566,6 +4671,7 @@ def test_liquidation_price_is_none( is_short=is_short, amount=71200.81144, stake_amount=open_rate * 71200.81144, + leverage=5, wallet_balance=-56354.57, mm_ex_1=0.10, upnl_ex_1=0.0 @@ -4586,7 +4692,7 @@ def test_liquidation_price_is_none( ("binance", False, 'futures', 'cross', 1535443.01, 356512.508, -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) ]) -def test_liquidation_price( +def test_liquidation_price_binance( mocker, default_conf, exchange_name, open_rate, is_short, trading_mode, margin_mode, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, amount, mm_ratio, expected ): @@ -4604,6 +4710,7 @@ def test_liquidation_price( upnl_ex_1=upnl_ex_1, amount=amount, stake_amount=open_rate * amount, + leverage=5, ), 2)) == expected @@ -4716,7 +4823,7 @@ def test_get_max_pair_stake_amount( }, } - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch(f'{EXMS}.markets', markets) assert exchange.get_max_pair_stake_amount('XRP/USDT:USDT', 2.0) == 20000 assert exchange.get_max_pair_stake_amount('XRP/USDT:USDT', 2.0, 5) == 4000 assert exchange.get_max_pair_stake_amount('LTC/USDT:USDT', 2.0) == float('inf') @@ -4726,7 +4833,7 @@ def test_get_max_pair_stake_amount( default_conf['trading_mode'] = 'spot' exchange = get_patched_exchange(mocker, default_conf, api_mock) - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch(f'{EXMS}.markets', markets) assert exchange.get_max_pair_stake_amount('BTC/USDT', 2.0) == 20000 assert exchange.get_max_pair_stake_amount('ADA/USDT', 2.0) == 500 @@ -4737,7 +4844,7 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name api_mock.fetch_leverage_tiers = MagicMock() type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') + mocker.patch(f'{EXMS}.validate_trading_mode_and_margin_mode') api_mock.fetch_leverage_tiers = MagicMock(return_value={ 'ADA/USDT:USDT': [ @@ -4815,7 +4922,6 @@ def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name ) -@pytest.mark.asyncio @pytest.mark.parametrize('exchange_name', EXCHANGES) async def test_get_market_leverage_tiers(mocker, default_conf, exchange_name): default_conf['exchange']['name'] = exchange_name @@ -4890,30 +4996,30 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage api_mock = MagicMock() default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._leverage_tiers = leverage_tiers with pytest.raises( - OperationalException, + DependencyException, match='nominal value can not be lower than 0', ): - exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', -1) + exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT:USDT', -1) exchange._leverage_tiers = {} with pytest.raises( InvalidOrderException, - match="Maintenance margin rate for 1000SHIB/USDT is unavailable for", + match="Maintenance margin rate for 1000SHIB/USDT:USDT is unavailable for", ): - exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', 10000) + exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT:USDT', 10000) @pytest.mark.parametrize('pair,value,mmr,maintAmt', [ - ('ADA/BUSD', 500, 0.025, 0.0), - ('ADA/BUSD', 20000000, 0.5, 1527500.0), - ('ZEC/USDT', 500, 0.01, 0.0), - ('ZEC/USDT', 20000000, 0.5, 654500.0), + ('ADA/BUSD:BUSD', 500, 0.025, 0.0), + ('ADA/BUSD:BUSD', 20000000, 0.5, 1527500.0), + ('ZEC/USDT:USDT', 500, 0.01, 0.0), + ('ZEC/USDT:USDT', 20000000, 0.5, 654500.0), ]) def test_get_maintenance_ratio_and_amt( mocker, @@ -4927,7 +5033,7 @@ def test_get_maintenance_ratio_and_amt( api_mock = MagicMock() default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._leverage_tiers = leverage_tiers exchange.get_maintenance_ratio_and_amt(pair, value) == (mmr, maintAmt) @@ -4946,27 +5052,27 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): exchange._leverage_tiers = leverage_tiers - assert exchange.get_max_leverage("BNB/BUSD", 1.0) == 20.0 - assert exchange.get_max_leverage("BNB/USDT", 100.0) == 75.0 - assert exchange.get_max_leverage("BTC/USDT", 170.30) == 125.0 - assert pytest.approx(exchange.get_max_leverage("BNB/BUSD", 99999.9)) == 5.000005 - assert pytest.approx(exchange.get_max_leverage("BNB/USDT", 1500)) == 33.333333333333333 - assert exchange.get_max_leverage("BTC/USDT", 300000000) == 2.0 - assert exchange.get_max_leverage("BTC/USDT", 600000000) == 1.0 # Last tier + assert exchange.get_max_leverage("BNB/BUSD:BUSD", 1.0) == 20.0 + assert exchange.get_max_leverage("BNB/USDT:USDT", 100.0) == 75.0 + assert exchange.get_max_leverage("BTC/USDT:USDT", 170.30) == 125.0 + assert pytest.approx(exchange.get_max_leverage("BNB/BUSD:BUSD", 99999.9)) == 5.000005 + assert pytest.approx(exchange.get_max_leverage("BNB/USDT:USDT", 1500)) == 33.333333333333333 + assert exchange.get_max_leverage("BTC/USDT:USDT", 300000000) == 2.0 + assert exchange.get_max_leverage("BTC/USDT:USDT", 600000000) == 1.0 # Last tier - assert exchange.get_max_leverage("SPONGE/USDT", 200) == 1.0 # Pair not in leverage_tiers - assert exchange.get_max_leverage("BTC/USDT", 0.0) == 125.0 # No stake amount + assert exchange.get_max_leverage("SPONGE/USDT:USDT", 200) == 1.0 # Pair not in leverage_tiers + assert exchange.get_max_leverage("BTC/USDT:USDT", 0.0) == 125.0 # No stake amount with pytest.raises( InvalidOrderException, - match=r'Amount 1000000000.01 too high for BTC/USDT' + match=r'Amount 1000000000.01 too high for BTC/USDT:USDT' ): - exchange.get_max_leverage("BTC/USDT", 1000000000.01) + exchange.get_max_leverage("BTC/USDT:USDT", 1000000000.01) -@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gateio', 'okx']) +@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'gate', 'okx', 'bybit']) def test__get_params(mocker, default_conf, exchange_name): api_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange._params = {'test': True} @@ -4984,6 +5090,9 @@ def test__get_params(mocker, default_conf, exchange_name): params2['tdMode'] = 'isolated' params2['posSide'] = 'net' + if exchange_name == 'bybit': + params2['position_idx'] = 0 + assert exchange._get_params( side="buy", ordertype='market', @@ -5025,6 +5134,7 @@ def test__get_params(mocker, default_conf, exchange_name): def test_get_liquidation_price1(mocker, default_conf): api_mock = MagicMock() + leverage = 9.97 positions = [ { 'info': {}, @@ -5037,7 +5147,7 @@ def test_get_liquidation_price1(mocker, default_conf): 'maintenanceMarginPercentage': 0.025, 'entryPrice': 18.884, 'notional': 15.1072, - 'leverage': 9.97, + 'leverage': leverage, 'unrealizedPnl': 0.0048, 'contracts': 8, 'contractSize': 0.1, @@ -5052,7 +5162,7 @@ def test_get_liquidation_price1(mocker, default_conf): ] api_mock.fetch_positions = MagicMock(return_value=positions) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), ) default_conf['dry_run'] = False @@ -5067,6 +5177,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price == 17.47 @@ -5079,6 +5190,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price == 17.540699999999998 @@ -5091,6 +5203,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price is None @@ -5104,17 +5217,18 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) -@pytest.mark.parametrize('liquidation_buffer', [0.0, 0.05]) +@pytest.mark.parametrize('liquidation_buffer', [0.0]) @pytest.mark.parametrize( "is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [ (False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), (True, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), - (False, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None), - (True, 'spot', 'gateio', '', 5.0, 10.0, 1.0, None), + (False, 'spot', 'gate', '', 5.0, 10.0, 1.0, None), + (True, 'spot', 'gate', '', 5.0, 10.0, 1.0, None), (False, 'spot', 'okx', '', 5.0, 10.0, 1.0, None), (True, 'spot', 'okx', '', 5.0, 10.0, 1.0, None), # Binance, short @@ -5127,16 +5241,26 @@ def test_get_liquidation_price1(mocker, default_conf): (False, 'futures', 'binance', 'isolated', 5, 8, 1.0, 6.454545454545454), (False, 'futures', 'binance', 'isolated', 3, 10, 1.0, 6.723905723905723), (False, 'futures', 'binance', 'isolated', 5, 10, 0.6, 8.063973063973064), - # Gateio/okx, short - (True, 'futures', 'gateio', 'isolated', 5, 10, 1.0, 11.87413417771621), - (True, 'futures', 'gateio', 'isolated', 5, 10, 2.0, 11.87413417771621), - (True, 'futures', 'gateio', 'isolated', 3, 10, 1.0, 13.193482419684678), - (True, 'futures', 'gateio', 'isolated', 5, 8, 1.0, 9.499307342172967), + # Gate/okx, short + (True, 'futures', 'gate', 'isolated', 5, 10, 1.0, 11.87413417771621), + (True, 'futures', 'gate', 'isolated', 5, 10, 2.0, 11.87413417771621), + (True, 'futures', 'gate', 'isolated', 3, 10, 1.0, 13.193482419684678), + (True, 'futures', 'gate', 'isolated', 5, 8, 1.0, 9.499307342172967), (True, 'futures', 'okx', 'isolated', 3, 10, 1.0, 13.193482419684678), - # Gateio/okx, long - (False, 'futures', 'gateio', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207), - (False, 'futures', 'gateio', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), + # Gate/okx, long + (False, 'futures', 'gate', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207), + (False, 'futures', 'gate', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), (False, 'futures', 'okx', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), + # bybit, long + (False, 'futures', 'bybit', 'isolated', 1.0, 10.0, 1.0, 0.1), + (False, 'futures', 'bybit', 'isolated', 3.0, 10.0, 1.0, 6.7666666), + (False, 'futures', 'bybit', 'isolated', 5.0, 10.0, 1.0, 8.1), + (False, 'futures', 'bybit', 'isolated', 10.0, 10.0, 1.0, 9.1), + # bybit, short + (True, 'futures', 'bybit', 'isolated', 1.0, 10.0, 1.0, 19.9), + (True, 'futures', 'bybit', 'isolated', 3.0, 10.0, 1.0, 13.233333), + (True, 'futures', 'bybit', 'isolated', 5.0, 10.0, 1.0, 11.9), + (True, 'futures', 'bybit', 'isolated', 10.0, 10.0, 1.0, 10.9), ] ) def test_get_liquidation_price( @@ -5182,7 +5306,7 @@ def test_get_liquidation_price( leverage = 5, open_rate = 10, amount = 0.6 ((1.6 + 0.01) - (1 * 0.6 * 10)) / ((0.6 * 0.01) - (1 * 0.6)) = 7.39057239057239 - Gateio/Okx, Short + Gate/Okx, Short leverage = 5, open_rate = 10, amount = 1.0 (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) (10 + (2 / 1.0)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 @@ -5193,7 +5317,7 @@ def test_get_liquidation_price( leverage = 5, open_rate = 8, amount = 1.0 (8 + (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 9.499307342172967 - Gateio/Okx, Long + Gate/Okx, Long leverage = 5, open_rate = 10, amount = 1.0 (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207 @@ -5208,7 +5332,7 @@ def test_get_liquidation_price( default_conf_usdt['trading_mode'] = trading_mode default_conf_usdt['exchange']['name'] = exchange_name default_conf_usdt['margin_mode'] = margin_mode - mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes') + mocker.patch('freqtrade.exchange.gate.Gate.validate_ordertypes') exchange = get_patched_exchange(mocker, default_conf_usdt, id=exchange_name) exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01)) @@ -5222,7 +5346,7 @@ def test_get_liquidation_price( amount=amount, stake_amount=amount * open_rate / leverage, wallet_balance=amount * open_rate / leverage, - # leverage=leverage, + leverage=leverage, is_short=is_short, ) if expected_liq is None: @@ -5253,14 +5377,14 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun 'symbol': 'ETH/BTC', }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.get_contract_size = MagicMock(return_value=contract_size) api_mock.create_order.reset_mock() - order = exchange.stoploss( + order = exchange.create_stoploss( pair='ETH/BTC', amount=100, stop_price=220, @@ -5274,3 +5398,10 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun assert order['cost'] == 100 assert order['filled'] == 100 assert order['remaining'] == 100 + + +def test_price_to_precision_with_default_conf(default_conf, mocker): + conf = copy.deepcopy(default_conf) + patched_ex = get_patched_exchange(mocker, conf) + prec_price = patched_ex.price_to_precision("XRP/USDT", 1.0000000101) + assert prec_price == 1.00000001 diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gate.py similarity index 65% rename from tests/exchange/test_gateio.py rename to tests/exchange/test_gate.py index dabdbba65..3cb5a9a3e 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gate.py @@ -4,45 +4,12 @@ from unittest.mock import MagicMock import pytest from freqtrade.enums import MarginMode, TradingMode -from freqtrade.exceptions import OperationalException -from freqtrade.exchange import Gateio -from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import get_patched_exchange - - -def test_validate_order_types_gateio(default_conf, mocker): - default_conf['exchange']['name'] = 'gateio' - mocker.patch('freqtrade.exchange.Exchange._init_ccxt') - mocker.patch('freqtrade.exchange.Exchange._load_markets', return_value={}) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs') - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') - mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') - exch = ExchangeResolver.load_exchange('gateio', default_conf, True) - assert isinstance(exch, Gateio) - - default_conf['order_types'] = { - 'entry': 'market', - 'exit': 'limit', - 'stoploss': 'market', - 'stoploss_on_exchange': False - } - - with pytest.raises(OperationalException, - match=r'Exchange .* does not support market orders.'): - ExchangeResolver.load_exchange('gateio', default_conf, True) - - # market-orders supported on futures markets. - default_conf['trading_mode'] = 'futures' - default_conf['margin_mode'] = 'isolated' - ex = ExchangeResolver.load_exchange('gateio', default_conf, True) - assert ex +from tests.conftest import EXMS, get_patched_exchange @pytest.mark.usefixtures("init_persistence") -def test_fetch_stoploss_order_gateio(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id='gateio') +def test_fetch_stoploss_order_gate(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id='gate') fetch_order_mock = MagicMock() exchange.fetch_order = fetch_order_mock @@ -56,7 +23,7 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker): default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' - exchange = get_patched_exchange(mocker, default_conf, id='gateio') + exchange = get_patched_exchange(mocker, default_conf, id='gate') exchange.fetch_order = MagicMock(return_value={ 'status': 'closed', @@ -73,8 +40,8 @@ def test_fetch_stoploss_order_gateio(default_conf, mocker): assert exchange.fetch_order.call_args_list[1][1]['order_id'] == '222555' -def test_cancel_stoploss_order_gateio(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id='gateio') +def test_cancel_stoploss_order_gate(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id='gate') cancel_order_mock = MagicMock() exchange.cancel_order = cancel_order_mock @@ -90,8 +57,8 @@ def test_cancel_stoploss_order_gateio(default_conf, mocker): (1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy") ]) -def test_stoploss_adjust_gateio(mocker, default_conf, sl1, sl2, sl3, side): - exchange = get_patched_exchange(mocker, default_conf, id='gateio') +def test_stoploss_adjust_gate(mocker, default_conf, sl1, sl2, sl3, side): + exchange = get_patched_exchange(mocker, default_conf, id='gate') order = { 'price': 1500, 'stopPrice': 1500, @@ -104,8 +71,8 @@ def test_stoploss_adjust_gateio(mocker, default_conf, sl1, sl2, sl3, side): ('taker', 0.0005, 0.0001554325), ('maker', 0.0, 0.0), ]) -def test_fetch_my_trades_gateio(mocker, default_conf, takerormaker, rate, cost): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) +def test_fetch_my_trades_gate(mocker, default_conf, takerormaker, rate, cost): + mocker.patch(f'{EXMS}.exchange_has', return_value=True) tick = {'ETH/USDT:USDT': { 'info': {'user_id': '', 'taker_fee': '0.0018', @@ -134,7 +101,7 @@ def test_fetch_my_trades_gateio(mocker, default_conf, takerormaker, rate, cost): 'takerOrMaker': takerormaker, 'amount': 1, # 1 contract }]) - exchange = get_patched_exchange(mocker, default_conf, api_mock=api_mock, id='gateio') + exchange = get_patched_exchange(mocker, default_conf, api_mock=api_mock, id='gate') exchange._trading_fees = tick trades = exchange.get_trades_for_order('22255', 'ETH/USDT:USDT', datetime.now(timezone.utc)) trade = trades[0] diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py index 2ce379a47..8be8ef8b3 100644 --- a/tests/exchange/test_huobi.py +++ b/tests/exchange/test_huobi.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException -from tests.conftest import get_patched_exchange +from freqtrade.exceptions import DependencyException, InvalidOrderException +from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -14,7 +14,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers (0.99, 220 * 0.99, "sell"), (0.98, 220 * 0.98, "sell"), ]) -def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side): +def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'stop-limit' @@ -26,21 +26,21 @@ def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side): } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}, - side=side, - leverage=1.0) + with pytest.raises(InvalidOrderException): + order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + side=side, + leverage=1.0) api_mock.create_order.reset_mock() order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types, - side=side, leverage=1.0) + order = exchange.create_stoploss( + pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types, side=side, leverage=1.0) assert 'id' in order assert 'info' in order @@ -59,40 +59,40 @@ def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={}, side=side, leverage=1.0) + exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={}, side=side, leverage=1.0) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={}, side=side, leverage=1.0) + exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={}, side=side, leverage=1.0) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi", - "stoploss", "create_order", retries=1, + "create_stoploss", "create_order", retries=1, pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side, leverage=1.0) -def test_stoploss_order_dry_run_huobi(default_conf, mocker): +def test_create_stoploss_order_dry_run_huobi(default_conf, mocker): api_mock = MagicMock() order_type = 'stop-limit' default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') - with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}, - side='sell', leverage=1.0) + with pytest.raises(InvalidOrderException): + order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + side='sell', leverage=1.0) api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={}, side='sell', leverage=1.0) + order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={}, side='sell', leverage=1.0) assert 'id' in order assert 'info' in order diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 66006f2fe..8fc23b94e 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -5,7 +5,7 @@ import ccxt import pytest from freqtrade.exceptions import DependencyException, InvalidOrderException -from tests.conftest import get_patched_exchange +from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -28,8 +28,8 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") order = exchange.create_order( @@ -68,8 +68,8 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, @@ -179,7 +179,7 @@ def test_get_balances_prod(default_conf, mocker): ("sell", 217.8), ("buy", 222.2), ]) -def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice): +def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -191,12 +191,12 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - order = exchange.stoploss( + order = exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, @@ -230,7 +230,7 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss( + exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, @@ -243,7 +243,7 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss( + exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, @@ -253,23 +253,23 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", - "stoploss", "create_order", retries=1, + "create_stoploss", "create_order", retries=1, pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side, leverage=1.0) @pytest.mark.parametrize('side', ['buy', 'sell']) -def test_stoploss_order_dry_run_kraken(default_conf, mocker, side): +def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') api_mock.create_order.reset_mock() - order = exchange.stoploss( + order = exchange.create_stoploss( pair='ETH/BTC', amount=1, stop_price=220, diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index ebaf5ae81..741ee27be 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException -from tests.conftest import get_patched_exchange +from freqtrade.exceptions import DependencyException, InvalidOrderException +from tests.conftest import EXMS, get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -15,7 +15,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers (0.99, 220 * 0.99, "sell"), (0.98, 220 * 0.98, "sell"), ]) -def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type): +def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -26,24 +26,24 @@ def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') if order_type == 'limit': - with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={ - 'stoploss': order_type, - 'stoploss_on_exchange_limit_ratio': 1.05}, - side=side, leverage=1.0) + with pytest.raises(InvalidOrderException): + order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={ + 'stoploss': order_type, + 'stoploss_on_exchange_limit_ratio': 1.05}, + side=side, leverage=1.0) api_mock.create_order.reset_mock() order_types = {'stoploss': order_type} if limitratio is not None: order_types.update({'stoploss_on_exchange_limit_ratio': limitratio}) - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types=order_types, side=side, leverage=1.0) + order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types=order_types, side=side, leverage=1.0) assert 'id' in order assert 'info' in order @@ -67,18 +67,18 @@ def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={}, side=side, leverage=1.0) + exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={}, side=side, leverage=1.0) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={}, side=side, leverage=1.0) + exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={}, side=side, leverage=1.0) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin", - "stoploss", "create_order", retries=1, + "create_stoploss", "create_order", retries=1, pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side, leverage=1.0) @@ -87,21 +87,21 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker): api_mock = MagicMock() order_type = 'market' default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') - with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss': 'limit', - 'stoploss_on_exchange_limit_ratio': 1.05}, - side='sell', leverage=1.0) + with pytest.raises(InvalidOrderException): + order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss': 'limit', + 'stoploss_on_exchange_limit_ratio': 1.05}, + side='sell', leverage=1.0) api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={}, side='sell', leverage=1.0) + order = exchange.create_stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={}, side='sell', leverage=1.0) assert 'id' in order assert 'info' in order @@ -125,3 +125,45 @@ def test_stoploss_adjust_kucoin(mocker, default_conf): # Test with invalid order case order['stopPrice'] = None assert exchange.stoploss_adjust(1501, order, 'sell') + + +@pytest.mark.parametrize("side", ["buy", "sell"]) +@pytest.mark.parametrize("ordertype,rate", [ + ("market", None), + ("market", 200), + ("limit", 200), + ("stop_loss_limit", 200) +]) +def test_kucoin_create_order(default_conf, mocker, side, ordertype, rate): + api_mock = MagicMock() + order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + }, + 'symbol': 'XRP/USDT', + 'amount': 1 + }) + default_conf['dry_run'] = False + mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='kucoin') + exchange._set_leverage = MagicMock() + exchange.set_margin_mode = MagicMock() + + order = exchange.create_order( + pair='XRP/USDT', + ordertype=ordertype, + side=side, + amount=1, + rate=rate, + leverage=1.0 + ) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert order['amount'] == 1 + # Status must be faked to open for kucoin. + assert order['status'] == 'open' diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index ac5c81ebb..3824eddb7 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -1,12 +1,14 @@ from datetime import datetime, timedelta, timezone from pathlib import Path -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock +import ccxt import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.exceptions import RetryableOrderError, TemporaryError 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_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -46,7 +48,7 @@ def test_get_maintenance_ratio_and_amt_okx( default_conf['margin_mode'] = 'isolated' default_conf['dry_run'] = False mocker.patch.multiple( - 'freqtrade.exchange.Okx', + 'freqtrade.exchange.okx.Okx', exchange_has=MagicMock(return_value=True), load_leverage_tiers=MagicMock(return_value={ 'ETH/USDT:USDT': [ @@ -195,12 +197,12 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers): exchange = get_patched_exchange(mocker, default_conf, id="okx") exchange._leverage_tiers = leverage_tiers - assert exchange.get_max_pair_stake_amount('BNB/BUSD', 1.0) == 30000000 - assert exchange.get_max_pair_stake_amount('BNB/USDT', 1.0) == 50000000 - assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0) == 1000000000 - assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0, 10.0) == 100000000 + assert exchange.get_max_pair_stake_amount('BNB/BUSD:BUSD', 1.0) == 30000000 + assert exchange.get_max_pair_stake_amount('BNB/USDT:USDT', 1.0) == 50000000 + assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0) == 1000000000 + assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0, 10.0) == 100000000 - assert exchange.get_max_pair_stake_amount('TTT/USDT', 1.0) == float('inf') # Not in tiers + assert exchange.get_max_pair_stake_amount('TTT/USDT:USDT', 1.0) == float('inf') # Not in tiers @pytest.mark.parametrize('mode,side,reduceonly,result', [ @@ -276,7 +278,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, 'fetchLeverageTiers': False, 'fetchMarketLeverageTiers': True, }) - api_mock.fetch_market_leverage_tiers = get_mock_coro(side_effect=[ + api_mock.fetch_market_leverage_tiers = AsyncMock(side_effect=[ [ { 'tier': 1, @@ -339,6 +341,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, } }, ], + TemporaryError("this Failed"), [ { 'tier': 1, @@ -476,3 +479,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/freqai/conftest.py b/tests/freqai/conftest.py index bee7df27e..4c4891ceb 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -1,5 +1,7 @@ +import platform from copy import deepcopy from pathlib import Path +from typing import Any, Dict from unittest.mock import MagicMock import pytest @@ -13,6 +15,11 @@ from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver from tests.conftest import get_patched_exchange +def is_mac() -> bool: + machine = platform.system() + return "Darwin" in machine + + @pytest.fixture(scope="function") def freqai_conf(default_conf, tmpdir): freqaiconf = deepcopy(default_conf) @@ -27,7 +34,7 @@ def freqai_conf(default_conf, tmpdir): "timerange": "20180110-20180115", "freqai": { "enabled": True, - "purge_old_models": True, + "purge_old_models": 2, "train_period_days": 2, "backtest_period_days": 10, "live_retrain_hours": 0, @@ -35,6 +42,7 @@ def freqai_conf(default_conf, tmpdir): "identifier": "uniqe-id100", "live_trained_timestamp": 0, "data_kitchen_thread_count": 2, + "activate_tensorboard": False, "feature_parameters": { "include_timeframes": ["5m"], "include_corr_pairlist": ["ADA/BTC"], @@ -46,6 +54,8 @@ def freqai_conf(default_conf, tmpdir): "use_SVM_to_remove_outliers": True, "stratify_training_data": 0, "indicator_periods_candles": [10], + "shuffle_after_split": False, + "buffer_train_data_candles": 0 }, "data_split_parameters": {"test_size": 0.33, "shuffle": False}, "model_training_parameters": {"n_estimators": 100}, @@ -76,11 +86,29 @@ def make_rl_config(conf): "rr": 1, "profit_aim": 0.02, "win_reward_factor": 2 - }} + }, + "drop_ohlc_from_features": False + } return conf +def mock_pytorch_mlp_model_training_parameters() -> Dict[str, Any]: + return { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, + "model_kwargs": { + "hidden_dim": 32, + "dropout_percent": 0.2, + "n_layer": 1, + } + } + + def get_patched_data_kitchen(mocker, freqaiconf): dk = FreqaiDataKitchen(freqaiconf) return dk @@ -115,6 +143,7 @@ def make_unfiltered_dataframe(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.pair = "ADA/BTC" data_load_timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) @@ -148,6 +177,7 @@ def make_data_dictionary(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.pair = "ADA/BTC" data_load_timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) diff --git a/tests/freqai/test_freqai_backtesting.py b/tests/freqai/test_freqai_backtesting.py index 60963e762..0a8059966 100644 --- a/tests/freqai/test_freqai_backtesting.py +++ b/tests/freqai/test_freqai_backtesting.py @@ -35,8 +35,8 @@ def test_freqai_backtest_start_backtest_list(freqai_conf, mocker, testdatadir, c args = get_args(args) bt_config = setup_optimize_configuration(args, RunMode.BACKTEST) Backtesting(bt_config) - assert log_has_re('Using --strategy-list with FreqAI REQUIRES all strategies to have identical ' - 'populate_any_indicators.', caplog) + assert log_has_re('Using --strategy-list with FreqAI REQUIRES all strategies to have identical', + caplog) Backtesting.cleanup() diff --git a/tests/freqai/test_freqai_datadrawer.py b/tests/freqai/test_freqai_datadrawer.py index da3b8f9c1..8ab2c75da 100644 --- a/tests/freqai/test_freqai_datadrawer.py +++ b/tests/freqai/test_freqai_datadrawer.py @@ -19,6 +19,7 @@ def test_update_historic_data(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -41,6 +42,7 @@ def test_load_all_pairs_histories(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -60,6 +62,7 @@ def test_get_base_and_corr_dataframes(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180111-20180114") @@ -87,6 +90,7 @@ def test_use_strategy_to_populate_indicators(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180111-20180114") @@ -103,8 +107,9 @@ def test_get_timerange_from_live_historic_predictions(mocker, freqai_conf): exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) freqai = strategy.freqai - freqai.live = True + freqai.live = False freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = False timerange = TimeRange.parse_timerange("20180126-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180128-20180130") diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 95665a775..13dc6b4b0 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -12,6 +12,7 @@ from freqtrade.freqai.data_kitchen import FreqaiDataKitchen from tests.conftest import get_patched_exchange, log_has_re from tests.freqai.conftest import (get_patched_data_kitchen, get_patched_freqai_strategy, make_data_dictionary, make_unfiltered_dataframe) +from tests.freqai.test_freqai_interface import is_mac @pytest.mark.parametrize( @@ -173,6 +174,9 @@ def test_get_full_model_path(mocker, freqai_conf, model): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": "freqai_test_strat"}) + if is_mac(): + pytest.skip("Mac is confused during this test for unknown reasons") + strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -180,6 +184,7 @@ def test_get_full_model_path(mocker, freqai_conf, model): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -187,7 +192,7 @@ def test_get_full_model_path(mocker, freqai_conf, model): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") - + freqai.dk.set_paths('ADA/BTC', None) freqai.extract_data_and_train_model( new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 4ef99720a..61a7b7346 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -1,5 +1,6 @@ import platform import shutil +import sys from pathlib import Path from unittest.mock import MagicMock @@ -13,8 +14,13 @@ from freqtrade.freqai.utils import download_all_data_for_training, get_required_ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager -from tests.conftest import create_mock_trades, get_patched_exchange, log_has_re -from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config +from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re +from tests.freqai.conftest import (get_patched_freqai_strategy, is_mac, make_rl_config, + mock_pytorch_mlp_model_training_parameters) + + +def is_py11() -> bool: + return sys.version_info >= (3, 11) def is_arm() -> bool: @@ -22,29 +28,36 @@ def is_arm() -> bool: return "arm" in machine or "aarch64" in machine -def is_mac() -> bool: - machine = platform.system() - return "Darwin" in machine +def can_run_model(model: str) -> None: + if is_arm() and "Catboost" in model: + pytest.skip("CatBoost is not supported on ARM.") + + is_pytorch_model = 'Reinforcement' in model or 'PyTorch' in model + if is_pytorch_model and is_mac() and not is_arm(): + pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.") -@pytest.mark.parametrize('model, pca, dbscan, float32, can_short', [ - ('LightGBMRegressor', True, False, True, True), - ('XGBoostRegressor', False, True, False, True), - ('XGBoostRFRegressor', False, False, False, True), - ('CatboostRegressor', False, False, False, True), - ('ReinforcementLearner', False, True, False, True), - ('ReinforcementLearner_multiproc', False, False, False, True), - ('ReinforcementLearner_test_3ac', False, False, False, False), - ('ReinforcementLearner_test_3ac', False, False, False, True), - ('ReinforcementLearner_test_4ac', False, False, False, True) +@pytest.mark.parametrize('model, pca, dbscan, float32, can_short, shuffle, buffer', [ + ('LightGBMRegressor', True, False, True, True, False, 0), + ('XGBoostRegressor', False, True, False, True, False, 10), + ('XGBoostRFRegressor', False, False, False, True, False, 0), + ('CatboostRegressor', False, False, False, True, True, 0), + ('PyTorchMLPRegressor', False, False, False, False, False, 0), + ('PyTorchTransformerRegressor', False, False, False, False, False, 0), + ('ReinforcementLearner', False, True, False, True, False, 0), + ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), + ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), + ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, - dbscan, float32, can_short): - if is_arm() and model == 'CatboostRegressor': - pytest.skip("CatBoost is not supported on ARM") + dbscan, float32, can_short, shuffle, buffer): - if is_mac() and not is_arm() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning module not available on intel based Mac OS") + can_run_model(model) + + test_tb = True + if is_mac(): + test_tb = False model_save_ext = 'joblib' freqai_conf.update({"freqaimodel": model}) @@ -53,13 +66,8 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, freqai_conf['freqai']['feature_parameters'].update({"principal_component_analysis": pca}) freqai_conf['freqai']['feature_parameters'].update({"use_DBSCAN_to_remove_outliers": dbscan}) freqai_conf.update({"reduce_df_footprint": float32}) - - if 'ReinforcementLearner' in model: - model_save_ext = 'zip' - freqai_conf = make_rl_config(freqai_conf) - # test the RL guardrails - freqai_conf['freqai']['feature_parameters'].update({"use_SVM_to_remove_outliers": True}) - freqai_conf['freqai']['data_split_parameters'].update({'shuffle': True}) + freqai_conf['freqai']['feature_parameters'].update({"shuffle_after_split": shuffle}) + freqai_conf['freqai']['feature_parameters'].update({"buffer_train_data_candles": buffer}) if 'ReinforcementLearner' in model: model_save_ext = 'zip' @@ -70,6 +78,15 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, if 'test_3ac' in model or 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + freqai_conf["freqai"]["rl_config"]["drop_ohlc_from_features"] = True + + if 'PyTorch' in model: + model_save_ext = 'zip' + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + if 'Transformer' in model: + # transformer model takes a window, unlike the MLP regressor + freqai_conf.update({"conv_width": 10}) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) @@ -77,8 +94,10 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, strategy.freqai_info = freqai_conf.get("freqai", {}) freqai = strategy.freqai freqai.live = True + freqai.activate_tensorboard = test_tb freqai.can_short = can_short freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.set_paths('ADA/BTC', 10000) timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -114,8 +133,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, ('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat") ]) def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat): - if is_arm() and 'Catboost' in model: - pytest.skip("CatBoost is not supported on ARM") + can_run_model(model) freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": strat}) @@ -127,6 +145,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -154,10 +173,10 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', + 'PyTorchMLPClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): - if is_arm() and model == 'CatboostClassifier': - pytest.skip("CatBoost is not supported on ARM") + can_run_model(model) freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"strategy": "freqai_test_classifier"}) @@ -170,6 +189,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -182,7 +202,20 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() + if 'PyTorchMLPClassifier': + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + + if freqai.dd.model_type == 'joblib': + model_file_extension = ".joblib" + elif freqai.dd.model_type == "pytorch": + model_file_extension = ".zip" + else: + raise Exception(f"Unsupported model type: {freqai.dd.model_type}," + f" can't assign model_file_extension") + + assert Path(freqai.dk.data_path / + f"{freqai.dk.model_filename}_model{model_file_extension}").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() @@ -196,20 +229,24 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): ("LightGBMRegressor", 2, "freqai_test_strat"), ("XGBoostRegressor", 2, "freqai_test_strat"), ("CatboostRegressor", 2, "freqai_test_strat"), + ("PyTorchMLPRegressor", 2, "freqai_test_strat"), + ("PyTorchTransformerRegressor", 2, "freqai_test_strat"), ("ReinforcementLearner", 3, "freqai_rl_test_strat"), ("XGBoostClassifier", 2, "freqai_test_classifier"), ("LightGBMClassifier", 2, "freqai_test_classifier"), - ("CatboostClassifier", 2, "freqai_test_classifier") + ("CatboostClassifier", 2, "freqai_test_classifier"), + ("PyTorchMLPClassifier", 2, "freqai_test_classifier") ], ) def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog): + can_run_model(model) + test_tb = True + if is_mac(): + test_tb = False + freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) freqai_conf['runmode'] = RunMode.BACKTEST - if is_arm() and "Catboost" in model: - pytest.skip("CatBoost is not supported on ARM") - if is_mac() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning module not available on intel based Mac OS") Trade.use_db = False freqai_conf.update({"freqaimodel": model}) @@ -222,6 +259,13 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) if 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + if 'PyTorch' in model: + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + if 'Transformer' in model: + # transformer model takes a window, unlike the MLP regressor + freqai_conf.update({"conv_width": 10}) + freqai_conf.get("freqai", {}).get("feature_parameters", {}).update( {"indicator_periods_candles": [2]}) @@ -231,6 +275,7 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) strategy.freqai_info = freqai_conf.get("freqai", {}) freqai = strategy.freqai freqai.live = False + freqai.activate_tensorboard = test_tb freqai.dk = FreqaiDataKitchen(freqai_conf) timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -242,6 +287,7 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) df[f'%-constant_{i}'] = i metadata = {"pair": "LTC/BTC"} + freqai.dk.set_paths('LTC/BTC', None) freqai.start_backtesting(df, metadata, freqai.dk, strategy) model_folders = [x for x in freqai.dd.full_path.iterdir() if x.is_dir()] @@ -365,6 +411,9 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog): sub_timerange = TimeRange.parse_timerange("20180129-20180130") corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk) df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + df = strategy.set_freqai_targets(df.copy(), metadata={"pair": "LTC/BTC"}) + df = freqai.dk.remove_special_chars_from_feature_names(df) + freqai.dk.get_unique_classes_from_labels(df) freqai.dk.pair = "ADA/BTC" freqai.dk.full_df = df.fillna(0) freqai.dk.full_df @@ -376,57 +425,6 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog): shutil.rmtree(Path(freqai.dk.full_path)) -def test_follow_mode(mocker, freqai_conf): - freqai_conf.update({"timerange": "20180110-20180130"}) - - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - metadata = {"pair": "ADA/BTC"} - freqai.dd.set_pair_dict_info(metadata) - - data_load_timerange = TimeRange.parse_timerange("20180110-20180130") - new_timerange = TimeRange.parse_timerange("20180120-20180130") - - freqai.extract_data_and_train_model( - new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").is_file() - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").is_file() - - # start the follower and ask it to predict on existing files - - freqai_conf.get("freqai", {}).update({"follow_mode": "true"}) - - strategy = get_patched_freqai_strategy(mocker, freqai_conf) - exchange = get_patched_exchange(mocker, freqai_conf) - strategy.dp = DataProvider(freqai_conf, exchange) - strategy.freqai_info = freqai_conf.get("freqai", {}) - freqai = strategy.freqai - freqai.live = True - freqai.dk = FreqaiDataKitchen(freqai_conf, freqai.live) - timerange = TimeRange.parse_timerange("20180110-20180130") - freqai.dd.load_all_pair_histories(timerange, freqai.dk) - - df = strategy.dp.get_pair_dataframe('ADA/BTC', '5m') - - freqai.dk.pair = "ADA/BTC" - freqai.start_live(df, metadata, strategy, freqai.dk) - - assert len(freqai.dk.return_dataframe.index) == 5702 - - shutil.rmtree(Path(freqai.dk.full_path)) - - def test_principal_component_analysis(mocker, freqai_conf): freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.get("freqai", {}).get("feature_parameters", {}).update( @@ -439,6 +437,7 @@ def test_principal_component_analysis(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -446,6 +445,7 @@ def test_principal_component_analysis(mocker, freqai_conf): data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") + freqai.dk.set_paths('ADA/BTC', None) freqai.extract_data_and_train_model( new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) @@ -470,13 +470,16 @@ def test_plot_feature_importance(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) - freqai.dd.pair_dict = MagicMock() + freqai.dd.pair_dict = {"ADA/BTC": {"model_filename": "fake_name", + "trained_timestamp": 1, "data_path": "", "extras": {}}} data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") + freqai.dk.set_paths('ADA/BTC', None) freqai.extract_data_and_train_model( new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) @@ -557,6 +560,8 @@ def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers): if is_mac(): pytest.skip("Reinforcement learning module not available on intel based Mac OS") + if is_py11(): + pytest.skip("Reinforcement learning currently not available on python 3.11.") freqai_conf.update({"freqaimodel": "ReinforcementLearner"}) freqai_conf.update({"timerange": "20180110-20180130"}) @@ -568,7 +573,7 @@ def test_get_state_info(mocker, freqai_conf, dp_exists, caplog, tickers): strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) ticker_mock = MagicMock(return_value=tickers()['ETH/BTC']) - mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) + mocker.patch(f"{EXMS}.fetch_ticker", ticker_mock) strategy.dp = DataProvider(freqai_conf, exchange) if not dp_exists: diff --git a/tests/freqai/test_models/ReinforcementLearner_test_3ac.py b/tests/freqai/test_models/ReinforcementLearner_test_3ac.py index c267c76a8..f77120c3c 100644 --- a/tests/freqai/test_models/ReinforcementLearner_test_3ac.py +++ b/tests/freqai/test_models/ReinforcementLearner_test_3ac.py @@ -18,6 +18,11 @@ class ReinforcementLearner_test_3ac(ReinforcementLearner): """ User can override any function in BaseRLEnv and gym.Env. Here the user sets a custom reward based on profit and trade duration. + + Warning! + This is function is a showcase of functionality designed to show as many possible + environment control features as possible. It is also designed to run quickly + on small computers. This is a benchmark, it is *not* for live production. """ def calculate_reward(self, action: int) -> float: diff --git a/tests/freqai/test_models/ReinforcementLearner_test_4ac.py b/tests/freqai/test_models/ReinforcementLearner_test_4ac.py index 29e3e3b64..4fc2b0005 100644 --- a/tests/freqai/test_models/ReinforcementLearner_test_4ac.py +++ b/tests/freqai/test_models/ReinforcementLearner_test_4ac.py @@ -18,6 +18,11 @@ class ReinforcementLearner_test_4ac(ReinforcementLearner): """ User can override any function in BaseRLEnv and gym.Env. Here the user sets a custom reward based on profit and trade duration. + + Warning! + This is function is a showcase of functionality designed to show as many possible + environment control features as possible. It is also designed to run quickly + on small computers. This is a benchmark, it is *not* for live production. """ def calculate_reward(self, action: int) -> float: diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index a3dd59004..b95764ba5 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -1,13 +1,14 @@ +from datetime import timedelta from typing import Dict, List, NamedTuple, Optional -import arrow from pandas import DataFrame from freqtrade.enums import ExitType from freqtrade.exchange import timeframe_to_minutes +from freqtrade.util.datetime_helpers import dt_utc -tests_start_time = arrow.get(2018, 10, 3) +tests_start_time = dt_utc(2018, 10, 3) tests_timeframe = '1h' @@ -46,7 +47,7 @@ class BTContainer(NamedTuple): def _get_frame_time_from_offset(offset): minutes = offset * timeframe_to_minutes(tests_timeframe) - return tests_start_time.shift(minutes=minutes).datetime + return tests_start_time + timedelta(minutes=minutes) def _build_backtest_dataframe(data): diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a18196507..158dd04dc 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -5,10 +5,10 @@ from unittest.mock import MagicMock import pytest from freqtrade.data.history import get_timerange -from freqtrade.enums import ExitType +from freqtrade.enums import ExitType, TradingMode from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence.trade_model import LocalTrade -from tests.conftest import patch_exchange +from tests.conftest import EXMS, patch_exchange from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset, tests_timeframe) @@ -919,17 +919,20 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) default_conf["trailing_stop_positive"] = data.trailing_stop_positive default_conf["trailing_stop_positive_offset"] = data.trailing_stop_positive_offset default_conf["use_exit_signal"] = data.use_exit_signal + default_conf["max_open_trades"] = 10 - mocker.patch("freqtrade.exchange.Exchange.get_fee", return_value=0.0) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch("freqtrade.exchange.Binance.get_max_leverage", return_value=100) + mocker.patch(f"{EXMS}.get_fee", return_value=0.0) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=100) + mocker.patch(f"{EXMS}.calculate_funding_fees", return_value=0) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) # TODO: Should we initialize this properly?? - backtesting._can_short = True + backtesting.trading_mode = TradingMode.MARGIN backtesting._set_strategy(backtesting.strategylist[0]) + backtesting._can_short = True backtesting.required_startup = 0 backtesting.strategy.advise_entry = lambda a, m: frame backtesting.strategy.advise_exit = lambda a, m: frame @@ -951,7 +954,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) processed=data_processed, start_date=min_date, end_date=max_date, - max_open_trades=10, ) results = result['results'] diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index fc14a0f88..bef942b43 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -9,7 +9,6 @@ from unittest.mock import MagicMock, PropertyMock import numpy as np import pandas as pd import pytest -from arrow import Arrow from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting @@ -19,15 +18,16 @@ from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange -from freqtrade.enums import ExitType, RunMode +from freqtrade.enums import CandleType, ExitType, RunMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.optimize.backtest_caching import get_strategy_run_id from freqtrade.optimize.backtesting import Backtesting -from freqtrade.persistence import LocalTrade +from freqtrade.persistence import LocalTrade, Trade from freqtrade.resolvers import StrategyResolver -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from freqtrade.util.datetime_helpers import dt_utc +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) ORDER_TYPES = [ @@ -96,7 +96,6 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): 'processed': processed, 'start_date': min_date, 'end_date': max_date, - 'max_open_trades': 10, } @@ -246,7 +245,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) def test_start(mocker, fee, default_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) patched_configuration_load_config_file(mocker, default_conf) @@ -270,7 +269,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: """ default_conf["order_types"] = order_types patch_exchange(mocker) - get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + get_fee = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) assert backtesting.config == default_conf @@ -291,7 +290,7 @@ def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'HyperoptableStrategy'] - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5)) with pytest.raises(OperationalException, match=r"Timeframe needs to be set in either configuration"): Backtesting(default_conf) @@ -301,7 +300,7 @@ def test_data_with_fee(default_conf, mocker) -> None: patch_exchange(mocker) default_conf['fee'] = 0.1234 - fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + fee_mock = mocker.patch(f'{EXMS}.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) assert backtesting.fee == 0.1234 @@ -345,9 +344,9 @@ def test_backtest_abort(default_conf, mocker, testdatadir) -> None: assert backtesting.progress.progress == 0 -def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: +def test_backtesting_start(default_conf, mocker, caplog) -> None: def get_timerange(input1): - return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) + return dt_utc(2017, 11, 14, 21, 17), dt_utc(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) @@ -355,12 +354,11 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats') - sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_signal_candles') + sbc = mocker.patch('freqtrade.optimize.backtesting.store_backtest_analysis_results') mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = '1m' - default_conf['datadir'] = testdatadir default_conf['export'] = 'signals' default_conf['exportfilename'] = 'export.txt' default_conf['timerange'] = '-1510694220' @@ -369,6 +367,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.bot_loop_start = MagicMock() + backtesting.strategy.bot_start = MagicMock() backtesting.start() # check the logs, that will contain the backtest result exists = [ @@ -378,14 +377,15 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: for line in exists: assert log_has(line, caplog) assert backtesting.strategy.dp._pairlists is not None - assert backtesting.strategy.bot_loop_start.call_count == 1 + assert backtesting.strategy.bot_start.call_count == 1 + assert backtesting.strategy.bot_loop_start.call_count == 0 assert sbs.call_count == 1 assert sbc.call_count == 1 def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: def get_timerange(input1): - return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) + return dt_utc(2017, 11, 14, 21, 17), dt_utc(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.history_utils.load_pair_history', MagicMock(return_value=pd.DataFrame())) @@ -396,7 +396,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = "1m" - default_conf['datadir'] = testdatadir default_conf['export'] = 'none' default_conf['timerange'] = '20180101-20180102' @@ -407,7 +406,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.data.history.history_utils.load_pair_history', MagicMock(return_value=pd.DataFrame())) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) @@ -417,7 +416,6 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> PropertyMock(return_value=[])) default_conf['timeframe'] = "1m" - default_conf['datadir'] = testdatadir default_conf['export'] = 'none' default_conf['timerange'] = '20180101-20180102' @@ -440,9 +438,9 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None: - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.get_tickers', tickers) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') @@ -451,7 +449,6 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist') default_conf['ticker_interval'] = "1m" - default_conf['datadir'] = testdatadir default_conf['export'] = 'none' # Use stoploss from strategy del default_conf['stoploss'] @@ -479,9 +476,9 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti def test_backtest__enter_trade(default_conf, fee, mocker) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.00001) + mocker.patch(f'{EXMS}.get_max_pair_stake_amount', return_value=float('inf')) patch_exchange(mocker) default_conf['stake_amount'] = 'unlimited' default_conf['max_open_trades'] = 2 @@ -530,7 +527,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert trade.stake_amount == 495 assert trade.is_short is True - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=300.0) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=300.0) trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade assert trade.stake_amount == 300.0 @@ -538,10 +535,10 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: default_conf_usdt['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=100) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=100) mocker.patch("freqtrade.optimize.backtesting.price_to_precision", lambda p, *args: p) patch_exchange(mocker) default_conf_usdt['stake_amount'] = 300 @@ -569,7 +566,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: ] backtesting.strategy.leverage = MagicMock(return_value=5.0) - mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.01, 0.01)) # leverage = 5 @@ -606,7 +603,7 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: assert pytest.approx(trade.liquidation_price) == 0.11787191 # Stake-amount too high! - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=600.0) trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None @@ -619,11 +616,11 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: assert trade is None -def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: +def test_backtest__check_trade_exit(default_conf, fee, mocker) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) default_conf['timeframe_detail'] = '1m' default_conf['max_open_trades'] = 2 @@ -665,7 +662,7 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: ] # No data available. - res = backtesting._get_exit_trade_entry(trade, row_sell, True) + res = backtesting._check_trade_exit(trade, row_sell) assert res is not None assert res.exit_reason == ExitType.ROI.value assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc) @@ -678,15 +675,17 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: [], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', 'enter_short', 'exit_short', 'long_tag', 'short_tag', 'exit_tag']) - res = backtesting._get_exit_trade_entry(trade, row, True) + res = backtesting._check_trade_exit(trade, row) assert res is None def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + default_conf['max_open_trades'] = 10 + + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -701,7 +700,6 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: processed=deepcopy(processed), start_date=min_date, end_date=max_date, - max_open_trades=10, ) results = result['results'] assert not results.empty @@ -712,11 +710,11 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'stake_amount': [0.001, 0.001], 'max_stake_amount': [0.001, 0.001], 'amount': [0.00957442, 0.0097064], - 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, - Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True + 'open_date': pd.to_datetime([dt_utc(2018, 1, 29, 18, 40, 0), + dt_utc(2018, 1, 30, 3, 30, 0)], utc=True ), - 'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime, - Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), + 'close_date': pd.to_datetime([dt_utc(2018, 1, 29, 22, 35, 0), + dt_utc(2018, 1, 30, 4, 10, 0)], utc=True), 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], 'fee_open': [0.0025, 0.0025], @@ -770,9 +768,9 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: @pytest.mark.parametrize('use_detail', [True, False]) def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None: default_conf_usdt['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) if use_detail: default_conf_usdt['timeframe_detail'] = '1m' patch_exchange(mocker) @@ -785,6 +783,8 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de def custom_entry_price(proposed_rate, **kwargs): return proposed_rate * 0.997 + default_conf_usdt['max_open_trades'] = 10 + backtesting = Backtesting(default_conf_usdt) backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.populate_entry_trend = advise_entry @@ -792,10 +792,10 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de pair = 'XRP/ETH' # Pick a timerange adapted to the pair we use to test timerange = TimeRange.parse_timerange('20191010-20191013') - data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['XRP/ETH'], + data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=[pair], timerange=timerange) if use_detail: - data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['XRP/ETH'], + data_1m = history.load_data(datadir=testdatadir, timeframe='1m', pairs=[pair], timerange=timerange) backtesting.detail_data = data_1m processed = backtesting.strategy.advise_all_indicators(data) @@ -805,7 +805,6 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de processed=deepcopy(processed), start_date=min_date, end_date=max_date, - max_open_trades=10, ) results = result['results'] assert not results.empty @@ -849,16 +848,175 @@ def test_backtest_one_detail(default_conf_usdt, fee, mocker, testdatadir, use_de assert late_entry > 0 +@pytest.mark.parametrize('use_detail', [True, False]) +def test_backtest_one_detail_futures( + default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None: + default_conf_usdt['use_exit_signal'] = False + default_conf_usdt['trading_mode'] = 'futures' + default_conf_usdt['margin_mode'] = 'isolated' + default_conf_usdt['candle_type_def'] = CandleType.FUTURES + + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['XRP/USDT:USDT'])) + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", + return_value=(0.01, 0.01)) + default_conf_usdt['timeframe'] = '1h' + if use_detail: + default_conf_usdt['timeframe_detail'] = '5m' + patch_exchange(mocker) + + def advise_entry(df, *args, **kwargs): + # Mock function to force several entries + df.loc[(df['rsi'] < 40), 'enter_long'] = 1 + return df + + def custom_entry_price(proposed_rate, **kwargs): + return proposed_rate * 0.997 + + default_conf_usdt['max_open_trades'] = 10 + + backtesting = Backtesting(default_conf_usdt) + backtesting._set_strategy(backtesting.strategylist[0]) + backtesting.strategy.populate_entry_trend = advise_entry + backtesting.strategy.custom_entry_price = custom_entry_price + pair = 'XRP/USDT:USDT' + # Pick a timerange adapted to the pair we use to test + timerange = TimeRange.parse_timerange('20211117-20211119') + data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair], + timerange=timerange, candle_type=CandleType.FUTURES) + backtesting.load_bt_data_detail() + processed = backtesting.strategy.advise_all_indicators(data) + min_date, max_date = get_timerange(processed) + + result = backtesting.backtest( + processed=deepcopy(processed), + start_date=min_date, + end_date=max_date, + ) + results = result['results'] + assert not results.empty + # Timeout settings from default_conf = entry: 10, exit: 30 + assert len(results) == (5 if use_detail else 2) + + assert 'orders' in results.columns + data_pair = processed[pair] + + data_1m_pair = backtesting.detail_data[pair] if use_detail else pd.DataFrame() + late_entry = 0 + for _, t in results.iterrows(): + assert len(t['orders']) == 2 + + entryo = t['orders'][0] + entry_ts = datetime.fromtimestamp(entryo['order_filled_timestamp'] // 1000, tz=timezone.utc) + if entry_ts > t['open_date']: + late_entry += 1 + + # Get "entry fill" candle + ln = (data_1m_pair.loc[data_1m_pair["date"] == entry_ts] + if use_detail else data_pair.loc[data_pair["date"] == entry_ts]) + # Check open trade rate aligns to open rate + assert not ln.empty + + assert round(ln.iloc[0]["low"], 6) <= round( + t["open_rate"], 6) <= round(ln.iloc[0]["high"], 6) + # check close trade rate aligns to close rate or is between high and low + ln1 = data_pair.loc[data_pair["date"] == t["close_date"]] + if use_detail: + ln1_1m = data_1m_pair.loc[data_1m_pair["date"] == t["close_date"]] + assert not ln1.empty or not ln1_1m.empty + else: + assert not ln1.empty + ln2 = ln1_1m if ln1.empty else ln1 + + assert (round(ln2.iloc[0]["low"], 6) <= round( + t["close_rate"], 6) <= round(ln2.iloc[0]["high"], 6)) + assert -0.0181 < Trade.trades[1].funding_fees < -0.01 + # assert late_entry > 0 + + +@pytest.mark.parametrize('use_detail', [True, False]) +def test_backtest_one_detail_futures_funding_fees( + default_conf_usdt, fee, mocker, testdatadir, use_detail) -> None: + default_conf_usdt['use_exit_signal'] = False + default_conf_usdt['trading_mode'] = 'futures' + default_conf_usdt['margin_mode'] = 'isolated' + default_conf_usdt['candle_type_def'] = CandleType.FUTURES + default_conf_usdt['minimal_roi'] = {'0': 1} + default_conf_usdt['dry_run_wallet'] = 100000 + + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['XRP/USDT:USDT'])) + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", + return_value=(0.01, 0.01)) + default_conf_usdt['timeframe'] = '1h' + if use_detail: + default_conf_usdt['timeframe_detail'] = '5m' + patch_exchange(mocker) + + def advise_entry(df, *args, **kwargs): + # Mock function to force several entries + df.loc[:, 'enter_long'] = 1 + return df + + def adjust_trade_position(trade, current_time, **kwargs): + if current_time > datetime(2021, 11, 18, 2, 0, 0, tzinfo=timezone.utc): + return None + return default_conf_usdt['stake_amount'] + + default_conf_usdt['max_open_trades'] = 1 + + backtesting = Backtesting(default_conf_usdt) + backtesting._set_strategy(backtesting.strategylist[0]) + backtesting.strategy.populate_entry_trend = advise_entry + backtesting.strategy.adjust_trade_position = adjust_trade_position + backtesting.strategy.leverage = lambda **kwargs: 1 + backtesting.strategy.position_adjustment_enable = True + pair = 'XRP/USDT:USDT' + # Pick a timerange adapted to the pair we use to test + timerange = TimeRange.parse_timerange('20211117-20211119') + data = history.load_data(datadir=Path(testdatadir), timeframe='1h', pairs=[pair], + timerange=timerange, candle_type=CandleType.FUTURES) + backtesting.load_bt_data_detail() + processed = backtesting.strategy.advise_all_indicators(data) + min_date, max_date = get_timerange(processed) + + result = backtesting.backtest( + processed=deepcopy(processed), + start_date=min_date, + end_date=max_date, + ) + results = result['results'] + assert not results.empty + # Only one result - as we're not selling. + assert len(results) == 1 + + assert 'orders' in results.columns + + for t in Trade.trades: + # At least 4 adjustment orders + assert t.nr_of_successful_entries >= 6 + # Funding fees will vary depending on the number of adjustment orders + # That number is a lot higher with detail data. + assert -20 < t.funding_fees < -0.1 + + def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) -> None: # This strategy intentionally places unfillable orders. default_conf['strategy'] = 'StrategyTestV3CustomEntryPrice' default_conf['startup_candle_count'] = 0 # Cancel unfilled order after 4 minutes on 5m timeframe. default_conf["unfilledtimeout"] = {"entry": 4} - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) + default_conf['max_open_trades'] = 1 backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) # Testing dataframe contains 11 candles. Expecting 10 timed out orders. @@ -871,7 +1029,6 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) processed=deepcopy(data), start_date=min_date, end_date=max_date, - max_open_trades=1, ) assert result['timedout_entry_orders'] == 10 @@ -879,9 +1036,10 @@ def test_backtest_timedout_entry_orders(default_conf, fee, mocker, testdatadir) def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + default_conf['max_open_trades'] = 1 + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -896,7 +1054,6 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None processed=processed, start_date=min_date, end_date=max_date, - max_open_trades=1, ) assert not results['results'].empty assert len(results['results']) == 1 @@ -904,9 +1061,11 @@ def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + default_conf['max_open_trades'] = 10 + + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -927,7 +1086,6 @@ def test_backtest_trim_no_data_left(default_conf, fee, mocker, testdatadir) -> N processed=deepcopy(processed), start_date=min_date, end_date=max_date, - max_open_trades=10, ) @@ -948,9 +1106,10 @@ def test_processed(default_conf, mocker, testdatadir) -> None: def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=100000) + default_conf['max_open_trades'] = 10 + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=100000) patch_exchange(mocker) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -981,7 +1140,6 @@ def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadi processed=deepcopy(processed), start_date=min_date, end_date=max_date, - max_open_trades=10, ) assert count == 5 @@ -998,9 +1156,10 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad default_conf['enable_protections'] = True default_conf['timeframe'] = '1m' - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + default_conf['max_open_trades'] = 1 + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) tests = [ ['sine', 9], ['raise', 10], @@ -1024,7 +1183,6 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad processed=processed, start_date=min_date, end_date=max_date, - max_open_trades=1, ) assert len(results['results']) == numres @@ -1047,9 +1205,9 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, default_conf['protections'] = protections default_conf['enable_protections'] = True - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) # While entry-signals are unrealistic, running backtesting # over and over again should not cause different results @@ -1062,11 +1220,12 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) assert isinstance(processed, dict) + backtesting.strategy.max_open_trades = 1 + backtesting.config.update({'max_open_trades': 1}) results = backtesting.backtest( processed=processed, start_date=min_date, end_date=max_date, - max_open_trades=1, ) assert len(results['results']) == expected @@ -1077,7 +1236,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): buy_value = 1 sell_value = 1 return _trend(dataframe, buy_value, sell_value) - + default_conf['max_open_trades'] = 10 backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -1094,6 +1253,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): sell_value = 1 return _trend(dataframe, buy_value, sell_value) + default_conf['max_open_trades'] = 10 backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -1104,9 +1264,10 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) + default_conf['max_open_trades'] = 10 backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC', datadir=testdatadir) default_conf['timeframe'] = '1m' @@ -1151,9 +1312,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) dataframe['exit_short'] = 0 return dataframe - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f'{EXMS}.get_fee', fee) patch_exchange(mocker) pairs = ['ADA/BTC', 'DASH/BTC', 'ETH/BTC', 'LTC/BTC', 'NXT/BTC'] @@ -1165,6 +1326,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) if tres > 0: data[pair] = data[pair][tres:].reset_index() default_conf['timeframe'] = '5m' + default_conf['max_open_trades'] = 3 backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) @@ -1173,11 +1335,11 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) + backtest_conf = { 'processed': deepcopy(processed), 'start_date': min_date, 'end_date': max_date, - 'max_open_trades': 3, } results = backtesting.backtest(**backtest_conf) @@ -1195,11 +1357,12 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting.dataprovider.get_analyzed_dataframe('NXT/BTC', '5m')[0] ) == len(data['NXT/BTC']) - 1 - backtesting.strategy.startup_candle_count + backtesting.strategy.max_open_trades = 1 + backtesting.config.update({'max_open_trades': 1}) backtest_conf = { 'processed': deepcopy(processed), 'start_date': min_date, 'end_date': max_date, - 'max_open_trades': 1, } results = backtesting.backtest(**backtest_conf) assert len(evaluate_result_multi(results['results'], '5m', 1)) == 0 @@ -1460,7 +1623,7 @@ def test_backtest_start_futures_noliq(default_conf_usdt, mocker, patch_exchange(mocker) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', - PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT'])) + PropertyMock(return_value=['HULUMULU/USDT', 'XRP/USDT:USDT'])) # mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) patched_configuration_load_config_file(mocker, default_conf_usdt) @@ -1491,7 +1654,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, "strategy": CURRENT_TEST_STRATEGY, }) patch_exchange(mocker) - result1 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT'], + result1 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT'], 'profit_ratio': [0.0, 0.0], 'profit_abs': [0.0, 0.0], 'open_date': pd.to_datetime(['2021-11-18 18:00:00', @@ -1507,7 +1670,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, 'close_rate': [0.104969, 0.103541], 'exit_reason': [ExitType.ROI, ExitType.ROI] }) - result2 = pd.DataFrame({'pair': ['XRP/USDT', 'XRP/USDT', 'XRP/USDT'], + result2 = pd.DataFrame({'pair': ['XRP/USDT:USDT', 'XRP/USDT:USDT', 'XRP/USDT:USDT'], 'profit_ratio': [0.03, 0.01, 0.1], 'profit_abs': [0.01, 0.02, 0.2], 'open_date': pd.to_datetime(['2021-11-19 18:00:00', @@ -1552,7 +1715,7 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, } ]) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', - PropertyMock(return_value=['XRP/USDT'])) + PropertyMock(return_value=['XRP/USDT:USDT'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) patched_configuration_load_config_file(mocker, default_conf_usdt) @@ -1575,8 +1738,8 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, 'up to 2021-11-21 04:00:00 (4 days).', 'Backtesting with data from 2021-11-17 21:00:00 ' 'up to 2021-11-21 04:00:00 (3 days).', - 'XRP/USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00', - 'XRP/USDT, mark, 8h, data starts at 2021-11-18 00:00:00', + 'XRP/USDT:USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00', + 'XRP/USDT:USDT, mark, 8h, data starts at 2021-11-18 00:00:00', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', ] diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 5c740458f..ce26e836e 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -5,23 +5,24 @@ from unittest.mock import MagicMock import pandas as pd import pytest -from arrow import Arrow from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import get_timerange -from freqtrade.enums import ExitType +from freqtrade.enums import ExitType, TradingMode from freqtrade.optimize.backtesting import Backtesting -from tests.conftest import patch_exchange +from freqtrade.util.datetime_helpers import dt_utc +from tests.conftest import EXMS, patch_exchange def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + default_conf['max_open_trades'] = 10 + mocker.patch(f'{EXMS}.get_fee', fee) mocker.patch('freqtrade.optimize.backtesting.amount_to_contract_precision', lambda x, *args, **kwargs: round(x, 8)) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=0.00001) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) patch_exchange(mocker) default_conf.update({ "stake_amount": 100.0, @@ -41,7 +42,6 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> processed=deepcopy(processed), start_date=min_date, end_date=max_date, - max_open_trades=10, ) results = result['results'] assert not results.empty @@ -52,11 +52,11 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> 'stake_amount': [500.0, 100.0], 'max_stake_amount': [500.0, 100], 'amount': [4806.87657523, 970.63960782], - 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, - Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True + 'open_date': pd.to_datetime([dt_utc(2018, 1, 29, 18, 40, 0), + dt_utc(2018, 1, 30, 3, 30, 0)], utc=True ), - 'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 00, 0).datetime, - Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), + 'close_date': pd.to_datetime([dt_utc(2018, 1, 29, 22, 00, 0), + dt_utc(2018, 1, 30, 4, 10, 0)], utc=True), 'open_rate': [0.10401764894444211, 0.10302485], 'close_rate': [0.10453904066847439, 0.103541], 'fee_open': [0.0025, 0.0025], @@ -99,18 +99,19 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> ]) def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, leverage) -> None: default_conf['use_exit_signal'] = False - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=10) - mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) - mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=10) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) patch_exchange(mocker) default_conf.update({ "stake_amount": 100.0, "dry_run_wallet": 1000.0, - "strategy": "StrategyTestV3" + "strategy": "StrategyTestV3", }) backtesting = Backtesting(default_conf) + backtesting.trading_mode = TradingMode.FUTURES backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) pair = 'XRP/USDT' diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 8241a5362..64172bf1c 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge from freqtrade.enums import RunMode from freqtrade.optimize.edge_cli import EdgeCli -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, log_has, patch_exchange, patched_configuration_load_config_file) @@ -71,7 +71,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N def test_start(mocker, fee, edge_conf, caplog) -> None: start_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) patch_exchange(mocker) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) patched_configuration_load_config_file(mocker, edge_conf) @@ -101,7 +101,7 @@ def test_edge_init_fee(mocker, edge_conf) -> None: patch_exchange(mocker) edge_conf['fee'] = 0.1234 edge_conf['stake_amount'] = 20 - fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) + fee_mock = mocker.patch(f'{EXMS}.get_fee', return_value=0.5) edge_cli = EdgeCli(edge_conf) assert edge_cli.edge.fee == 0.1234 assert fee_mock.call_count == 0 diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 5bce9f419..ed5eeafd6 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,12 +1,13 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 from datetime import datetime, timedelta +from functools import wraps from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock import pandas as pd import pytest -from arrow import Arrow from filelock import Timeout +from skopt.space import Integer from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data @@ -18,7 +19,8 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IntParameter -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_markets, log_has, log_has_re, +from freqtrade.util import dt_utc +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, get_args, get_markets, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -292,6 +294,8 @@ def test_params_no_optimize_details(hyperopt) -> None: assert res['roi']['0'] == 0.04 assert "stoploss" in res assert res['stoploss']['stoploss'] == -0.1 + assert "max_open_trades" in res + assert res['max_open_trades']['max_open_trades'] == 1 def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: @@ -334,8 +338,7 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: assert dumper2.call_count == 1 assert hasattr(hyperopt.backtesting.strategy, "advise_exit") assert hasattr(hyperopt.backtesting.strategy, "advise_entry") - assert hasattr(hyperopt, "max_open_trades") - assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] + assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt.backtesting, "_position_stacking") @@ -346,14 +349,14 @@ def test_hyperopt_format_results(hyperopt): "UNITTEST/BTC", "UNITTEST/BTC"], "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_date": [dt_utc(2017, 11, 14, 19, 32, 00), + dt_utc(2017, 11, 14, 21, 36, 00), + dt_utc(2017, 11, 14, 22, 12, 00), + dt_utc(2017, 11, 14, 22, 44, 00)], + "close_date": [dt_utc(2017, 11, 14, 21, 35, 00), + dt_utc(2017, 11, 14, 22, 10, 00), + dt_utc(2017, 11, 14, 22, 43, 00), + dt_utc(2017, 11, 14, 22, 58, 00)], "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "trade_duration": [123, 34, 31, 14], @@ -376,8 +379,8 @@ def test_hyperopt_format_results(hyperopt): 'backtest_end_time': 1619718665, } results_metrics = generate_strategy_stats(['XRP/BTC'], '', bt_result, - Arrow(2017, 11, 14, 19, 32, 00), - Arrow(2017, 12, 14, 19, 32, 00), market_change=0) + dt_utc(2017, 11, 14, 19, 32, 00), + dt_utc(2017, 12, 14, 19, 32, 00), market_change=0) results_explanation = HyperoptTools.format_results_explanation_string(results_metrics, 'BTC') total_profit = results_metrics['profit_total_abs'] @@ -420,14 +423,14 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: "UNITTEST/BTC", "UNITTEST/BTC"], "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_date": [dt_utc(2017, 11, 14, 19, 32, 00), + dt_utc(2017, 11, 14, 21, 36, 00), + dt_utc(2017, 11, 14, 22, 12, 00), + dt_utc(2017, 11, 14, 22, 44, 00)], + "close_date": [dt_utc(2017, 11, 14, 21, 35, 00), + dt_utc(2017, 11, 14, 22, 10, 00), + dt_utc(2017, 11, 14, 22, 43, 00), + dt_utc(2017, 11, 14, 22, 58, 00)], "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "trade_duration": [123, 34, 31, 14], @@ -450,7 +453,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: mocker.patch('freqtrade.optimize.hyperopt.Backtesting.backtest', return_value=backtest_result) mocker.patch('freqtrade.optimize.hyperopt.get_timerange', - return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) + return_value=(dt_utc(2017, 12, 10), dt_utc(2017, 12, 13))) patch_exchange(mocker) mocker.patch.object(Path, 'open') mocker.patch('freqtrade.configuration.config_validation.validate_config_schema') @@ -474,6 +477,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing_stop_positive': 0.02, 'trailing_stop_positive_offset_p1': 0.05, 'trailing_only_offset_is_reached': False, + 'max_open_trades': 3, } response_expected = { 'loss': 1.9147239021396234, @@ -499,7 +503,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'trailing': {'trailing_only_offset_is_reached': False, 'trailing_stop': True, 'trailing_stop_positive': 0.02, - 'trailing_stop_positive_offset': 0.07}}, + 'trailing_stop_positive_offset': 0.07}, + 'max_open_trades': {'max_open_trades': 3} + }, 'params_dict': optimizer_param, 'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}}, 'results_metrics': ANY, @@ -507,8 +513,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: } hyperopt = Hyperopt(hyperopt_conf) - hyperopt.min_date = Arrow(2017, 12, 10) - hyperopt.max_date = Arrow(2017, 12, 13) + hyperopt.min_date = dt_utc(2017, 12, 10) + hyperopt.max_date = dt_utc(2017, 12, 13) hyperopt.init_spaces() generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected @@ -548,7 +554,8 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: 'buy': {'mfi-value': None}, 'sell': {'sell-mfi-value': None}, 'roi': {}, 'stoploss': {'stoploss': None}, - 'trailing': {'trailing_stop': None} + 'trailing': {'trailing_stop': None}, + 'max_open_trades': {'max_open_trades': None} }, 'results_metrics': generate_result_metrics(), }]) @@ -571,7 +578,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: out, err = capsys.readouterr() result_str = ( '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"' - ':{},"stoploss":null,"trailing_stop":null}' + ':{},"stoploss":null,"trailing_stop":null,"max_open_trades":null}' ) assert result_str in out # noqa: E501 # Should be called for historical candle data @@ -702,8 +709,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non assert hasattr(hyperopt.backtesting.strategy, "advise_exit") assert hasattr(hyperopt.backtesting.strategy, "advise_entry") - assert hasattr(hyperopt, "max_open_trades") - assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] + assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt.backtesting, "_position_stacking") @@ -776,8 +782,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: assert dumper2.call_count == 1 assert hasattr(hyperopt.backtesting.strategy, "advise_exit") assert hasattr(hyperopt.backtesting.strategy, "advise_entry") - assert hasattr(hyperopt, "max_open_trades") - assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] + assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt.backtesting, "_position_stacking") @@ -819,8 +824,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: assert dumper2.call_count == 1 assert hasattr(hyperopt.backtesting.strategy, "advise_exit") assert hasattr(hyperopt.backtesting.strategy, "advise_entry") - assert hasattr(hyperopt, "max_open_trades") - assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] + assert hyperopt.backtesting.strategy.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt.backtesting, "_position_stacking") @@ -855,7 +859,7 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None: def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed hyperopt_conf.update({ @@ -868,12 +872,14 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 assert hyperopt.backtesting.strategy.sell_rsi.value == 74 assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30 + assert hyperopt.backtesting.strategy.max_open_trades == 1 buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range assert isinstance(buy_rsi_range, range) # Range from 0 - 50 (inclusive) @@ -884,6 +890,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30 assert hyperopt.backtesting.strategy.buy_rsi.value != 35 assert hyperopt.backtesting.strategy.sell_rsi.value != 74 + assert hyperopt.backtesting.strategy.max_open_trades != 1 hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1' with pytest.raises(OperationalException, match="Estimator ET1 not supported."): @@ -891,10 +898,10 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None: - mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch('freqtrade.exchange.Exchange._load_markets') - mocker.patch('freqtrade.exchange.Exchange.markets', + mocker.patch(f'{EXMS}.validate_config', MagicMock()) + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f'{EXMS}._load_markets') + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=get_markets())) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed @@ -916,7 +923,8 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 @@ -932,7 +940,7 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee) -> None: patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch(f'{EXMS}.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ @@ -953,7 +961,8 @@ def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee) hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) - assert hyperopt.backtesting.strategy.bot_loop_started is True + assert hyperopt.backtesting.strategy.bot_loop_started is False + assert hyperopt.backtesting.strategy.bot_started is True assert hyperopt.backtesting.strategy.buy_rsi.in_space is True assert hyperopt.backtesting.strategy.buy_rsi.value == 35 @@ -984,3 +993,124 @@ def test_SKDecimal(): assert space.transform([2.0]) == [200] assert space.transform([1.0]) == [100] assert space.transform([1.5, 1.6]) == [150, 160] + + +def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, fee) -> None: + # This test is to ensure that unlimited max_open_trades are ignored for the backtesting + # if we have an unlimited stake amount + patch_exchange(mocker) + mocker.patch(f'{EXMS}.get_fee', fee) + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + 'hyperopt_random_state': 42, + 'spaces': ['trades'], + 'stake_amount': 'unlimited' + }) + hyperopt = Hyperopt(hyperopt_conf) + mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict', + return_value={ + 'max_open_trades': -1 + }) + + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + + assert hyperopt.backtesting.strategy.max_open_trades == 1 + + hyperopt.start() + + assert hyperopt.backtesting.strategy.max_open_trades == 1 + + +def test_max_open_trades_dump(mocker, hyperopt_conf, tmpdir, fee, capsys) -> None: + # This test is to ensure that after hyperopting, max_open_trades is never + # saved as inf in the output json params + patch_exchange(mocker) + mocker.patch(f'{EXMS}.get_fee', fee) + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + 'hyperopt_random_state': 42, + 'spaces': ['trades'], + }) + hyperopt = Hyperopt(hyperopt_conf) + mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict', + return_value={ + 'max_open_trades': -1 + }) + + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + + hyperopt.start() + + out, err = capsys.readouterr() + + assert 'max_open_trades = -1' in out + assert 'max_open_trades = inf' not in out + + ############## + + hyperopt_conf.update({'print_json': True}) + + hyperopt = Hyperopt(hyperopt_conf) + mocker.patch('freqtrade.optimize.hyperopt.Hyperopt._get_params_dict', + return_value={ + 'max_open_trades': -1 + }) + + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + + hyperopt.start() + + out, err = capsys.readouterr() + + assert '"max_open_trades":-1' in out + + +def test_max_open_trades_consistency(mocker, hyperopt_conf, tmpdir, fee) -> None: + # This test is to ensure that max_open_trades is the same across all functions needing it + # after it has been changed from the hyperopt + patch_exchange(mocker) + mocker.patch(f'{EXMS}.get_fee', return_value=0) + + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + 'hyperopt_random_state': 42, + 'spaces': ['trades'], + 'stake_amount': 'unlimited', + 'dry_run_wallet': 8, + 'available_capital': 8, + 'dry_run': True, + 'epochs': 1 + }) + hyperopt = Hyperopt(hyperopt_conf) + + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + + hyperopt.custom_hyperopt.max_open_trades_space = lambda: [ + Integer(1, 10, name='max_open_trades')] + + first_time_evaluated = False + + def stake_amount_interceptor(func): + @wraps(func) + def wrapper(*args, **kwargs): + nonlocal first_time_evaluated + stake_amount = func(*args, **kwargs) + if first_time_evaluated is False: + assert stake_amount == 1 + first_time_evaluated = True + return stake_amount + return wrapper + + hyperopt.backtesting.wallets._calculate_unlimited_stake_amount = stake_amount_interceptor( + hyperopt.backtesting.wallets._calculate_unlimited_stake_amount) + + hyperopt.start() + + assert hyperopt.backtesting.strategy.max_open_trades == 8 + assert hyperopt.config['max_open_trades'] == 8 diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 7d4fef3bd..eace78eee 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -66,52 +66,58 @@ def test_load_previous_results2(mocker, testdatadir, caplog) -> None: @pytest.mark.parametrize("spaces, expected_results", [ (['buy'], {'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False, - 'protection': False}), + 'protection': False, 'trades': False}), (['sell'], {'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False, - 'protection': False}), + 'protection': False, 'trades': False}), (['roi'], {'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False, - 'protection': False}), + 'protection': False, 'trades': False}), (['stoploss'], {'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False, - 'protection': False}), + 'protection': False, 'trades': False}), (['trailing'], {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True, - 'protection': False}), + 'protection': False, 'trades': False}), (['buy', 'sell', 'roi', 'stoploss'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False, - 'protection': False}), + 'protection': False, 'trades': False}), (['buy', 'sell', 'roi', 'stoploss', 'trailing'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, - 'protection': False}), + 'protection': False, 'trades': False}), (['buy', 'roi'], {'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False, - 'protection': False}), + 'protection': False, 'trades': False}), (['all'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, - 'protection': True}), + 'protection': True, 'trades': True}), (['default'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False, - 'protection': False}), + 'protection': False, 'trades': False}), (['default', 'trailing'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, - 'protection': False}), + 'protection': False, 'trades': False}), (['all', 'buy'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, - 'protection': True}), + 'protection': True, 'trades': True}), (['default', 'buy'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False, - 'protection': False}), + 'protection': False, 'trades': False}), (['all'], {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, - 'protection': True}), + 'protection': True, 'trades': True}), (['protection'], {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False, - 'protection': True}), + 'protection': True, 'trades': False}), + (['trades'], + {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False, + 'protection': False, 'trades': True}), + (['default', 'trades'], + {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False, + 'protection': False, 'trades': True}), ]) def test_has_space(hyperopt_conf, spaces, expected_results): - for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection']: + for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'trades']: hyperopt_conf.update({'spaces': spaces}) assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s] @@ -193,6 +199,9 @@ def test_export_params(tmpdir): "346": 0.08499, "507": 0.049, "1595": 0 + }, + "max_open_trades": { + "max_open_trades": 5 } }, "params_not_optimized": { @@ -219,6 +228,7 @@ def test_export_params(tmpdir): assert "roi" in content["params"] assert "stoploss" in content["params"] assert "trailing" in content["params"] + assert "max_open_trades" in content["params"] def test_try_export_params(default_conf, tmpdir, caplog, mocker): @@ -297,6 +307,9 @@ def test_params_print(capsys): "trailing_stop_positive_offset": 0.1, "trailing_only_offset_is_reached": True }, + "max_open_trades": { + "max_open_trades": 5 + } } HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized) @@ -327,6 +340,13 @@ def test_params_print(capsys): assert re.search('trailing_stop_positive_offset = 0.1 # value loaded.*\n', captured.out) assert re.search('trailing_only_offset_is_reached = True # value loaded.*\n', captured.out) + HyperoptTools._params_pretty_print( + params, 'max_open_trades', "Max Open Trades:", non_optimized) + captured = capsys.readouterr() + + assert re.search("# Max Open Trades:", captured.out) + assert re.search('max_open_trades = 5 # value loaded.*\n', captured.out) + def test_hyperopt_serializer(): diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 549202284..82e8a46fb 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -6,10 +6,9 @@ from shutil import copyfile import joblib import pandas as pd import pytest -from arrow import Arrow from freqtrade.configuration import TimeRange -from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN +from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data import history from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, load_backtest_stats) @@ -21,10 +20,12 @@ from freqtrade.optimize.optimize_reports import (_get_resample_from_period, gene generate_periodic_breakdown_stats, generate_strategy_comparison, generate_trading_stats, show_sorted_pairlist, - store_backtest_signal_candles, + store_backtest_analysis_results, store_backtest_stats, text_table_bt_results, text_table_exit_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver +from freqtrade.util import dt_ts +from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc from tests.conftest import CURRENT_TEST_STRATEGY from tests.data.test_history import _clean_test_file @@ -80,14 +81,14 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): "UNITTEST/BTC", "UNITTEST/BTC"], "profit_ratio": [0.003312, 0.010801, 0.013803, 0.002780], "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_date": [dt_utc(2017, 11, 14, 19, 32, 00), + dt_utc(2017, 11, 14, 21, 36, 00), + dt_utc(2017, 11, 14, 22, 12, 00), + dt_utc(2017, 11, 14, 22, 44, 00)], + "close_date": [dt_utc(2017, 11, 14, 21, 35, 00), + dt_utc(2017, 11, 14, 22, 10, 00), + dt_utc(2017, 11, 14, 22, 43, 00), + dt_utc(2017, 11, 14, 22, 58, 00)], "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], "trade_duration": [123, 34, 31, 14], @@ -106,14 +107,14 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): 'canceled_trade_entries': 0, 'canceled_entry_orders': 0, 'replaced_entry_orders': 0, - 'backtest_start_time': Arrow.utcnow().int_timestamp, - 'backtest_end_time': Arrow.utcnow().int_timestamp, + 'backtest_start_time': dt_ts() // 1000, + 'backtest_end_time': dt_ts() // 1000, 'run_id': '123', } } timerange = TimeRange.parse_timerange('1510688220-1510700340') - min_date = Arrow.fromtimestamp(1510688220) - max_date = Arrow.fromtimestamp(1510700340) + min_date = dt_from_ts(1510688220) + max_date = dt_from_ts(1510700340) btdata = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, fill_up_missing=True) @@ -135,14 +136,14 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], "profit_ratio": [0.003312, 0.010801, -0.013803, 0.002780], "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_date": [dt_utc(2017, 11, 14, 19, 32, 00), + dt_utc(2017, 11, 14, 21, 36, 00), + dt_utc(2017, 11, 14, 22, 12, 00), + dt_utc(2017, 11, 14, 22, 44, 00)], + "close_date": [dt_utc(2017, 11, 14, 21, 35, 00), + dt_utc(2017, 11, 14, 22, 10, 00), + dt_utc(2017, 11, 14, 22, 43, 00), + dt_utc(2017, 11, 14, 22, 58, 00)], "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], "trade_duration": [123, 34, 31, 14], @@ -161,8 +162,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): 'canceled_trade_entries': 0, 'canceled_entry_orders': 0, 'replaced_entry_orders': 0, - 'backtest_start_time': Arrow.utcnow().int_timestamp, - 'backtest_end_time': Arrow.utcnow().int_timestamp, + 'backtest_start_time': dt_ts() // 1000, + 'backtest_end_time': dt_ts() // 1000, 'run_id': '124', } } @@ -232,20 +233,20 @@ def test_store_backtest_candles(testdatadir, mocker): candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}} # mock directory exporting - store_backtest_signal_candles(testdatadir, candle_dict, '2022_01_01_15_05_13') + store_backtest_analysis_results(testdatadir, candle_dict, {}, '2022_01_01_15_05_13') - assert dump_mock.call_count == 1 + assert dump_mock.call_count == 2 assert isinstance(dump_mock.call_args_list[0][0][0], Path) - assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) + assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl') dump_mock.reset_mock() # mock file exporting filename = Path(testdatadir / 'testresult') - store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13') - assert dump_mock.call_count == 1 + store_backtest_analysis_results(filename, candle_dict, {}, '2022_01_01_15_05_13') + assert dump_mock.call_count == 2 assert isinstance(dump_mock.call_args_list[0][0][0], Path) # result will be testdatadir / testresult-_signals.pkl - assert str(dump_mock.call_args_list[0][0][0]).endswith(str('_signals.pkl')) + assert str(dump_mock.call_args_list[0][0][0]).endswith('_signals.pkl') dump_mock.reset_mock() @@ -254,10 +255,11 @@ def test_write_read_backtest_candles(tmpdir): candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}} # test directory exporting - stored_file = store_backtest_signal_candles(Path(tmpdir), candle_dict, '2022_01_01_15_05_13') - scp = open(stored_file, "rb") - pickled_signal_candles = joblib.load(scp) - scp.close() + sample_date = '2022_01_01_15_05_13' + store_backtest_analysis_results(Path(tmpdir), candle_dict, {}, sample_date) + stored_file = Path(tmpdir / f'backtest-result-{sample_date}_signals.pkl') + with stored_file.open("rb") as scp: + pickled_signal_candles = joblib.load(scp) assert pickled_signal_candles.keys() == candle_dict.keys() assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys() @@ -268,10 +270,10 @@ def test_write_read_backtest_candles(tmpdir): # test file exporting filename = Path(tmpdir / 'testresult') - stored_file = store_backtest_signal_candles(filename, candle_dict, '2022_01_01_15_05_13') - scp = open(stored_file, "rb") - pickled_signal_candles = joblib.load(scp) - scp.close() + store_backtest_analysis_results(filename, candle_dict, {}, sample_date) + stored_file = Path(tmpdir / f'testresult-{sample_date}_signals.pkl') + with stored_file.open("rb") as scp: + pickled_signal_candles = joblib.load(scp) assert pickled_signal_candles.keys() == candle_dict.keys() assert pickled_signal_candles['DefStrat'].keys() == pickled_signal_candles['DefStrat'].keys() @@ -465,11 +467,14 @@ def test_generate_periodic_breakdown_stats(testdatadir): def test__get_resample_from_period(): assert _get_resample_from_period('day') == '1d' - assert _get_resample_from_period('week') == '1w' + assert _get_resample_from_period('week') == '1W-MON' assert _get_resample_from_period('month') == '1M' with pytest.raises(ValueError, match=r"Period noooo is not supported."): _get_resample_from_period('noooo') + for period in BACKTEST_BREAKDOWNS: + assert isinstance(_get_resample_from_period(period), str) + def test_show_sorted_pairlist(testdatadir, default_conf, capsys): filename = testdatadir / "backtest_results/backtest-result.json" diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py new file mode 100644 index 000000000..1dab8764a --- /dev/null +++ b/tests/persistence/test_key_value_store.py @@ -0,0 +1,69 @@ +from datetime import datetime, timedelta, timezone + +import pytest + +from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time +from tests.conftest import create_mock_trades_usdt + + +@pytest.mark.usefixtures("init_persistence") +def test_key_value_store(time_machine): + start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc) + time_machine.move_to(start, tick=False) + + KeyValueStore.store_value("test", "testStringValue") + KeyValueStore.store_value("test_dt", datetime.now(timezone.utc)) + KeyValueStore.store_value("test_float", 22.51) + KeyValueStore.store_value("test_int", 15) + + assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_string_value("test") == "testStringValue" + assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_datetime_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_string_value("test_dt") is None + assert KeyValueStore.get_float_value("test_dt") is None + assert KeyValueStore.get_int_value("test_dt") is None + assert KeyValueStore.get_value("test_float") == 22.51 + assert KeyValueStore.get_float_value("test_float") == 22.51 + assert KeyValueStore.get_value("test_int") == 15 + assert KeyValueStore.get_int_value("test_int") == 15 + assert KeyValueStore.get_datetime_value("test_int") is None + + time_machine.move_to(start + timedelta(days=20, hours=5), tick=False) + assert KeyValueStore.get_value("test_dt") != datetime.now(timezone.utc) + assert KeyValueStore.get_value("test_dt") == start + # Test update works + KeyValueStore.store_value("test_dt", datetime.now(timezone.utc)) + assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + + KeyValueStore.store_value("test_float", 23.51) + assert KeyValueStore.get_value("test_float") == 23.51 + # test deleting + KeyValueStore.delete_value("test_float") + assert KeyValueStore.get_value("test_float") is None + # Delete same value again (should not fail) + KeyValueStore.delete_value("test_float") + + with pytest.raises(ValueError, match=r"Unknown value type"): + KeyValueStore.store_value("test_float", {'some': 'dict'}) + + +@pytest.mark.usefixtures("init_persistence") +def test_set_startup_time(fee, time_machine): + create_mock_trades_usdt(fee) + start = datetime.now(timezone.utc) + time_machine.move_to(start, tick=False) + set_startup_time() + + assert KeyValueStore.get_value("startup_time") == start + initial_time = KeyValueStore.get_value("bot_start_time") + assert initial_time <= start + + # Simulate bot restart + new_start = start + timedelta(days=5) + time_machine.move_to(new_start, tick=False) + set_startup_time() + + assert KeyValueStore.get_value("startup_time") == new_start + assert KeyValueStore.get_value("bot_start_time") == initial_time diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 2a6959d58..13b3f89bf 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -1,15 +1,18 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +from importlib import import_module from pathlib import Path from unittest.mock import MagicMock import pytest -from sqlalchemy import create_engine, text +from sqlalchemy import create_engine, select, text +from sqlalchemy.schema import CreateTable from freqtrade.constants import DEFAULT_DB_PROD_URL from freqtrade.enums import TradingMode from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade, init_db +from freqtrade.persistence.base import ModelBase from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids from freqtrade.persistence.models import PairLock from tests.conftest import log_has @@ -21,8 +24,8 @@ spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURE def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url']) - assert hasattr(Trade, '_session') - assert 'scoped_session' in type(Trade._session).__name__ + assert hasattr(Trade, 'session') + assert 'scoped_session' in type(Trade.session).__name__ def test_init_custom_db_url(default_conf, tmpdir): @@ -34,7 +37,7 @@ def test_init_custom_db_url(default_conf, tmpdir): init_db(default_conf['db_url']) assert Path(filename).is_file() - r = Trade._session.execute(text("PRAGMA journal_mode")) + r = Trade.session.execute(text("PRAGMA journal_mode")) assert r.first() == ('wal',) @@ -235,8 +238,9 @@ def test_migrate_new(mocker, default_conf, fee, caplog): # Run init to test migration init_db(default_conf['db_url']) - assert len(Trade.query.filter(Trade.id == 1).all()) == 1 - trade = Trade.query.filter(Trade.id == 1).first() + trades = Trade.session.scalars(select(Trade).filter(Trade.id == 1)).all() + assert len(trades) == 1 + trade = trades[0] assert trade.fee_open == fee.return_value assert trade.fee_close == fee.return_value assert trade.open_rate_requested is None @@ -404,9 +408,20 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog): init_db(default_conf['db_url']) - assert len(PairLock.query.all()) == 2 - assert len(PairLock.query.filter(PairLock.pair == '*').all()) == 1 - pairlocks = PairLock.query.filter(PairLock.pair == 'ETH/BTC').all() + assert len(PairLock.get_all_locks().all()) == 2 + assert len(PairLock.session.scalars(select(PairLock).filter(PairLock.pair == '*')).all()) == 1 + pairlocks = PairLock.session.scalars(select(PairLock).filter(PairLock.pair == 'ETH/BTC')).all() assert len(pairlocks) == 1 pairlocks[0].pair == 'ETH/BTC' pairlocks[0].side == '*' + + +@pytest.mark.parametrize('dialect', [ + 'sqlite', 'postgresql', 'mysql', 'oracle', 'mssql', + ]) +def test_create_table_compiles(dialect): + + dialect_mod = import_module(f"sqlalchemy.dialects.{dialect}") + for table in ModelBase.metadata.tables.values(): + create_sql = str(CreateTable(table).compile(dialect=dialect_mod.dialect())) + assert 'CREATE TABLE' in create_sql diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index e12e919fc..4aa3b1e96 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -2,13 +2,14 @@ from datetime import datetime, timedelta, timezone from types import FunctionType -import arrow import pytest +from sqlalchemy import select -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException from freqtrade.persistence import LocalTrade, Order, Trade, init_db +from freqtrade.util import dt_now from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re @@ -26,7 +27,7 @@ def test_enter_exit_side(fee, is_short): open_rate=0.01, amount=5, is_open=True, - open_date=arrow.utcnow().datetime, + open_date=dt_now(), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -48,7 +49,7 @@ def test_set_stop_loss_liquidation(fee): open_rate=2.0, amount=30.0, is_open=True, - open_date=arrow.utcnow().datetime, + open_date=dt_now(), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -238,7 +239,7 @@ def test_interest(fee, exchange, is_short, lev, minutes, rate, interest, stake_amount=20.0, amount=30.0, open_rate=2.0, - open_date=datetime.utcnow() - timedelta(minutes=minutes), + open_date=datetime.now(timezone.utc) - timedelta(minutes=minutes), fee_open=fee.return_value, fee_close=fee.return_value, exchange=exchange, @@ -328,7 +329,7 @@ def test_borrowed(fee, is_short, lev, borrowed, trading_mode): open_rate=2.0, amount=30.0, is_open=True, - open_date=arrow.utcnow().datetime, + open_date=dt_now(), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -427,7 +428,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ open_rate=open_rate, amount=30.0, is_open=True, - open_date=arrow.utcnow().datetime, + open_date=dt_now(), fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -484,7 +485,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, is_open=True, fee_open=fee.return_value, fee_close=fee.return_value, - open_date=arrow.utcnow().datetime, + open_date=dt_now(), exchange='binance', trading_mode=margin, leverage=1.0, @@ -634,7 +635,7 @@ def test_trade_close(fee): assert pytest.approx(trade.close_profit) == 0.094513715 assert trade.close_date is not None - new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + new_date = datetime(2020, 2, 2, 15, 6, 1), assert trade.close_date != new_date # Close should NOT update close_date if the trade has been closed already assert trade.is_open is False @@ -1325,74 +1326,82 @@ def test_to_json(fee): amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, - open_date=arrow.utcnow().shift(hours=-2).datetime, + open_date=dt_now() - timedelta(hours=2), open_rate=0.123, exchange='binance', enter_tag=None, - open_order_id='dry_run_buy_12345' + open_order_id='dry_run_buy_12345', + precision_mode=1, + amount_precision=8.0, + price_precision=7.0, ) result = trade.to_json() assert isinstance(result, dict) - assert result == {'trade_id': None, - 'pair': 'ADA/USDT', - 'base_currency': 'ADA', - 'quote_currency': 'USDT', - 'is_open': None, - 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), - 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'open_order_id': 'dry_run_buy_12345', - 'close_date': None, - 'close_timestamp': None, - 'open_rate': 0.123, - 'open_rate_requested': None, - 'open_trade_value': 15.1668225, - 'fee_close': 0.0025, - 'fee_close_cost': None, - 'fee_close_currency': None, - 'fee_open': 0.0025, - 'fee_open_cost': None, - 'fee_open_currency': None, - 'close_rate': None, - 'close_rate_requested': None, - 'amount': 123.0, - 'amount_requested': 123.0, - 'stake_amount': 0.001, - 'max_stake_amount': None, - 'trade_duration': None, - 'trade_duration_s': None, - 'realized_profit': 0.0, - 'close_profit': None, - 'close_profit_pct': None, - 'close_profit_abs': None, - 'profit_ratio': None, - 'profit_pct': None, - 'profit_abs': None, - 'exit_reason': None, - 'exit_order_status': None, - 'stop_loss_abs': None, - 'stop_loss_ratio': None, - 'stop_loss_pct': None, - 'stoploss_order_id': None, - 'stoploss_last_update': None, - 'stoploss_last_update_timestamp': None, - 'initial_stop_loss_abs': None, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, - 'min_rate': None, - 'max_rate': None, - 'strategy': None, - 'enter_tag': None, - 'timeframe': None, - 'exchange': 'binance', - 'leverage': None, - 'interest_rate': None, - 'liquidation_price': None, - 'is_short': None, - 'trading_mode': None, - 'funding_fees': None, - 'orders': [], - } + assert result == { + 'trade_id': None, + 'pair': 'ADA/USDT', + 'base_currency': 'ADA', + 'quote_currency': 'USDT', + 'is_open': None, + 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), + 'open_timestamp': int(trade.open_date.timestamp() * 1000), + 'open_order_id': 'dry_run_buy_12345', + 'close_date': None, + 'close_timestamp': None, + 'open_rate': 0.123, + 'open_rate_requested': None, + 'open_trade_value': 15.1668225, + 'fee_close': 0.0025, + 'fee_close_cost': None, + 'fee_close_currency': None, + 'fee_open': 0.0025, + 'fee_open_cost': None, + 'fee_open_currency': None, + 'close_rate': None, + 'close_rate_requested': None, + 'amount': 123.0, + 'amount_requested': 123.0, + 'stake_amount': 0.001, + 'max_stake_amount': None, + 'trade_duration': None, + 'trade_duration_s': None, + 'realized_profit': 0.0, + 'realized_profit_ratio': None, + 'close_profit': None, + 'close_profit_pct': None, + 'close_profit_abs': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, + 'exit_reason': None, + 'exit_order_status': None, + 'stop_loss_abs': None, + 'stop_loss_ratio': None, + 'stop_loss_pct': None, + 'stoploss_order_id': None, + 'stoploss_last_update': None, + 'stoploss_last_update_timestamp': None, + 'initial_stop_loss_abs': None, + 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, + 'min_rate': None, + 'max_rate': None, + 'strategy': None, + 'enter_tag': None, + 'timeframe': None, + 'exchange': 'binance', + 'leverage': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, + 'trading_mode': None, + 'funding_fees': None, + 'amount_precision': 8.0, + 'price_precision': 7.0, + 'precision_mode': 1, + 'orders': [], + } # Simulate dry_run entries trade = Trade( @@ -1402,75 +1411,83 @@ def test_to_json(fee): amount_requested=101.0, fee_open=fee.return_value, fee_close=fee.return_value, - open_date=arrow.utcnow().shift(hours=-2).datetime, - close_date=arrow.utcnow().shift(hours=-1).datetime, + open_date=dt_now() - timedelta(hours=2), + close_date=dt_now() - timedelta(hours=1), open_rate=0.123, close_rate=0.125, enter_tag='buys_signal_001', exchange='binance', + precision_mode=2, + amount_precision=7.0, + price_precision=8.0, ) result = trade.to_json() assert isinstance(result, dict) - assert result == {'trade_id': None, - 'pair': 'XRP/BTC', - 'base_currency': 'XRP', - 'quote_currency': 'BTC', - 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), - 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), - 'close_timestamp': int(trade.close_date.timestamp() * 1000), - 'open_rate': 0.123, - 'close_rate': 0.125, - 'amount': 100.0, - 'amount_requested': 101.0, - 'stake_amount': 0.001, - 'max_stake_amount': None, - 'trade_duration': 60, - 'trade_duration_s': 3600, - 'stop_loss_abs': None, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, - 'stoploss_order_id': None, - 'stoploss_last_update': None, - 'stoploss_last_update_timestamp': None, - 'initial_stop_loss_abs': None, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, - 'realized_profit': 0.0, - 'close_profit': None, - 'close_profit_pct': None, - 'close_profit_abs': None, - 'profit_ratio': None, - 'profit_pct': None, - 'profit_abs': None, - 'close_rate_requested': None, - 'fee_close': 0.0025, - 'fee_close_cost': None, - 'fee_close_currency': None, - 'fee_open': 0.0025, - 'fee_open_cost': None, - 'fee_open_currency': None, - 'is_open': None, - 'max_rate': None, - 'min_rate': None, - 'open_order_id': None, - 'open_rate_requested': None, - 'open_trade_value': 12.33075, - 'exit_reason': None, - 'exit_order_status': None, - 'strategy': None, - 'enter_tag': 'buys_signal_001', - 'timeframe': None, - 'exchange': 'binance', - 'leverage': None, - 'interest_rate': None, - 'liquidation_price': None, - 'is_short': None, - 'trading_mode': None, - 'funding_fees': None, - 'orders': [], - } + assert result == { + 'trade_id': None, + 'pair': 'XRP/BTC', + 'base_currency': 'XRP', + 'quote_currency': 'BTC', + 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), + 'open_timestamp': int(trade.open_date.timestamp() * 1000), + 'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), + 'close_timestamp': int(trade.close_date.timestamp() * 1000), + 'open_rate': 0.123, + 'close_rate': 0.125, + 'amount': 100.0, + 'amount_requested': 101.0, + 'stake_amount': 0.001, + 'max_stake_amount': None, + 'trade_duration': 60, + 'trade_duration_s': 3600, + 'stop_loss_abs': None, + 'stop_loss_pct': None, + 'stop_loss_ratio': None, + 'stoploss_order_id': None, + 'stoploss_last_update': None, + 'stoploss_last_update_timestamp': None, + 'initial_stop_loss_abs': None, + 'initial_stop_loss_pct': None, + 'initial_stop_loss_ratio': None, + 'realized_profit': 0.0, + 'realized_profit_ratio': None, + 'close_profit': None, + 'close_profit_pct': None, + 'close_profit_abs': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, + 'close_rate_requested': None, + 'fee_close': 0.0025, + 'fee_close_cost': None, + 'fee_close_currency': None, + 'fee_open': 0.0025, + 'fee_open_cost': None, + 'fee_open_currency': None, + 'is_open': None, + 'max_rate': None, + 'min_rate': None, + 'open_order_id': None, + 'open_rate_requested': None, + 'open_trade_value': 12.33075, + 'exit_reason': None, + 'exit_order_status': None, + 'strategy': None, + 'enter_tag': 'buys_signal_001', + 'timeframe': None, + 'exchange': 'binance', + 'leverage': None, + 'interest_rate': None, + 'liquidation_price': None, + 'is_short': None, + 'trading_mode': None, + 'funding_fees': None, + 'amount_precision': 7.0, + 'price_precision': 8.0, + 'precision_mode': 2, + 'orders': [], + } def test_stoploss_reinitialization(default_conf, fee): @@ -1479,7 +1496,7 @@ def test_stoploss_reinitialization(default_conf, fee): pair='ADA/USDT', stake_amount=30.0, fee_open=fee.return_value, - open_date=arrow.utcnow().shift(hours=-2).datetime, + open_date=dt_now() - timedelta(hours=2), amount=30.0, fee_close=fee.return_value, exchange='binance', @@ -1492,7 +1509,7 @@ def test_stoploss_reinitialization(default_conf, fee): assert trade.stop_loss_pct == -0.05 assert trade.initial_stop_loss == 0.95 assert trade.initial_stop_loss_pct == -0.05 - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() # Lower stoploss @@ -1540,7 +1557,7 @@ def test_stoploss_reinitialization_leverage(default_conf, fee): pair='ADA/USDT', stake_amount=30.0, fee_open=fee.return_value, - open_date=arrow.utcnow().shift(hours=-2).datetime, + open_date=dt_now() - timedelta(hours=2), amount=30.0, fee_close=fee.return_value, exchange='binance', @@ -1554,7 +1571,7 @@ def test_stoploss_reinitialization_leverage(default_conf, fee): assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 0.98 assert trade.initial_stop_loss_pct == -0.1 - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() # Lower stoploss @@ -1602,7 +1619,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): pair='ADA/USDT', stake_amount=0.001, fee_open=fee.return_value, - open_date=arrow.utcnow().shift(hours=-2).datetime, + open_date=dt_now() - timedelta(hours=2), amount=10, fee_close=fee.return_value, exchange='binance', @@ -1616,7 +1633,7 @@ def test_stoploss_reinitialization_short(default_conf, fee): assert trade.stop_loss_pct == -0.1 assert trade.initial_stop_loss == 1.02 assert trade.initial_stop_loss_pct == -0.1 - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() # Lower stoploss Trade.stoploss_reinitialization(-0.15) @@ -1661,7 +1678,7 @@ def test_update_fee(fee): pair='ADA/USDT', stake_amount=30.0, fee_open=fee.return_value, - open_date=arrow.utcnow().shift(hours=-2).datetime, + open_date=dt_now() - timedelta(hours=2), amount=30.0, fee_close=fee.return_value, exchange='binance', @@ -1700,7 +1717,7 @@ def test_fee_updated(fee): pair='ADA/USDT', stake_amount=30.0, fee_open=fee.return_value, - open_date=arrow.utcnow().shift(hours=-2).datetime, + open_date=dt_now() - timedelta(hours=2), amount=30.0, fee_close=fee.return_value, exchange='binance', @@ -1791,17 +1808,17 @@ def test_get_trades_proxy(fee, use_db, is_short): @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize('is_short', [True, False]) def test_get_trades__query(fee, is_short): - query = Trade.get_trades([]) + query = Trade.get_trades_query([]) # without orders there should be no join issued. - query1 = Trade.get_trades([], include_orders=False) + query1 = Trade.get_trades_query([], include_orders=False) # Empty "with-options -> default - selectin" assert query._with_options == () assert query1._with_options != () create_mock_trades(fee, is_short) - query = Trade.get_trades([]) - query1 = Trade.get_trades([], include_orders=False) + query = Trade.get_trades_query([]) + query1 = Trade.get_trades_query([], include_orders=False) assert query._with_options == () assert query1._with_options != () @@ -1868,7 +1885,10 @@ def test_get_exit_order_count(fee, is_short): @pytest.mark.usefixtures("init_persistence") -def test_update_order_from_ccxt(caplog): +def test_update_order_from_ccxt(caplog, time_machine): + start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc) + time_machine.move_to(start, tick=False) + # Most basic order return (only has orderid) o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy', 20.01, 1234.6) assert isinstance(o, Order) @@ -1917,7 +1937,9 @@ def test_update_order_from_ccxt(caplog): assert o.filled == 20.0 assert o.remaining == 0.0 assert not o.ft_is_open - assert o.order_filled_date is not None + assert o.order_filled_date == start + # Move time + time_machine.move_to(start + timedelta(hours=1), tick=False) ccxt_order.update({'id': 'somethingelse'}) with pytest.raises(DependencyException, match=r"Order-id's don't match"): @@ -1930,6 +1952,12 @@ def test_update_order_from_ccxt(caplog): # Call regular update - shouldn't fail. Order.update_orders([o], {'id': '1234'}) + assert o.order_filled_date == start + + # Fill order again - shouldn't update filled date + ccxt_order.update({'id': '1234'}) + Order.update_orders([o], ccxt_order) + assert o.order_filled_date == start @pytest.mark.usefixtures("init_persistence") @@ -2003,11 +2031,13 @@ def test_Trade_object_idem(): 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', + 'get_trades_query', 'get_exit_reason_performance', 'get_enter_tag_performance', 'get_mix_tag_performance', 'get_trading_volume', 'from_json', + 'validate_string_len', ) EXCLUDES2 = ('trades', 'trades_open', 'bt_trades_open_pp', 'bt_open_open_trade_count', 'total_profit') @@ -2026,6 +2056,31 @@ def test_Trade_object_idem(): assert item in trade +@pytest.mark.usefixtures("init_persistence") +def test_trade_truncates_string_fields(): + trade = Trade( + pair='ADA/USDT', + stake_amount=20.0, + amount=30.0, + open_rate=2.0, + open_date=datetime.now(timezone.utc) - timedelta(minutes=20), + fee_open=0.001, + fee_close=0.001, + exchange='binance', + leverage=1.0, + trading_mode='futures', + enter_tag='a' * CUSTOM_TAG_MAX_LENGTH * 2, + exit_reason='b' * CUSTOM_TAG_MAX_LENGTH * 2, + ) + Trade.session.add(trade) + Trade.commit() + + trade1 = Trade.session.scalars(select(Trade)).first() + + assert trade1.enter_tag == 'a' * CUSTOM_TAG_MAX_LENGTH + assert trade1.exit_reason == 'b' * CUSTOM_TAG_MAX_LENGTH + + def test_recalc_trade_from_orders(fee): o1_amount = 100 @@ -2037,7 +2092,7 @@ def test_recalc_trade_from_orders(fee): trade = Trade( pair='ADA/USDT', stake_amount=o1_cost, - open_date=arrow.utcnow().shift(hours=-2).datetime, + open_date=dt_now() - timedelta(hours=2), amount=o1_amount, fee_open=fee.return_value, fee_close=fee.return_value, @@ -2112,8 +2167,8 @@ def test_recalc_trade_from_orders(fee): filled=o2_amount, remaining=0, cost=o2_cost, - order_date=arrow.utcnow().shift(hours=-1).datetime, - order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + order_date=dt_now() - timedelta(hours=1), + order_filled_date=dt_now() - timedelta(hours=1), ) trade.orders.append(order2) trade.recalc_trade_from_orders() @@ -2146,8 +2201,8 @@ def test_recalc_trade_from_orders(fee): filled=o3_amount, remaining=0, cost=o3_cost, - order_date=arrow.utcnow().shift(hours=-1).datetime, - order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + order_date=dt_now() - timedelta(hours=1), + order_filled_date=dt_now() - timedelta(hours=1), ) trade.orders.append(order3) trade.recalc_trade_from_orders() @@ -2202,7 +2257,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): trade = Trade( pair='ADA/USDT', stake_amount=o1_cost, - open_date=arrow.utcnow().shift(hours=-2).datetime, + open_date=dt_now() - timedelta(hours=2), amount=o1_amount, fee_open=fee.return_value, fee_close=fee.return_value, @@ -2254,8 +2309,8 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): filled=o1_amount, remaining=0, cost=o1_cost, - order_date=arrow.utcnow().shift(hours=-1).datetime, - order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + order_date=dt_now() - timedelta(hours=1), + order_filled_date=dt_now() - timedelta(hours=1), ) trade.orders.append(order2) trade.recalc_trade_from_orders() @@ -2282,8 +2337,8 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): filled=0, remaining=4, cost=5, - order_date=arrow.utcnow().shift(hours=-1).datetime, - order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + order_date=dt_now() - timedelta(hours=1), + order_filled_date=dt_now() - timedelta(hours=1), ) trade.orders.append(order3) trade.recalc_trade_from_orders() @@ -2309,8 +2364,8 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): filled=o1_amount, remaining=0, cost=o1_cost, - order_date=arrow.utcnow().shift(hours=-1).datetime, - order_filled_date=arrow.utcnow().shift(hours=-1).datetime, + order_date=dt_now() - timedelta(hours=1), + order_filled_date=dt_now() - timedelta(hours=1), ) trade.orders.append(order4) trade.recalc_trade_from_orders() @@ -2426,11 +2481,12 @@ def test_select_filled_orders(fee): @pytest.mark.usefixtures("init_persistence") -def test_order_to_ccxt(limit_buy_order_open): +def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open): order = Order.parse_from_ccxt_object(limit_buy_order_open, 'mocked', 'buy') - order.query.session.add(order) - Order.query.session.commit() + order.ft_trade_id = 1 + order.session.add(order) + Order.session.commit() order_resp = Order.order_by_id(limit_buy_order_open['id']) assert order_resp @@ -2439,11 +2495,23 @@ def test_order_to_ccxt(limit_buy_order_open): del raw_order['fee'] del raw_order['datetime'] del raw_order['info'] - assert raw_order['stopPrice'] is None - del raw_order['stopPrice'] + assert raw_order.get('stopPrice') is None + raw_order.pop('stopPrice', None) del limit_buy_order_open['datetime'] assert raw_order == limit_buy_order_open + order1 = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'mocked', 'sell') + order1.ft_order_side = 'stoploss' + order1.stop_price = order1.price * 0.9 + order1.ft_trade_id = 1 + order1.session.add(order1) + Order.session.commit() + + order_resp1 = Order.order_by_id(limit_sell_order_usdt_open['id']) + raw_order1 = order_resp1.to_ccxt_object() + + assert raw_order1.get('stopPrice') is not None + @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize('data', [ @@ -2524,7 +2592,7 @@ def test_recalc_trade_from_orders_dca(data) -> None: open_rate=data['orders'][0][0][2], amount=data['orders'][0][0][1], is_open=True, - open_date=arrow.utcnow().datetime, + open_date=dt_now(), fee_open=data['fee'], fee_close=data['fee'], exchange='binance', @@ -2532,7 +2600,7 @@ def test_recalc_trade_from_orders_dca(data) -> None: leverage=1.0, trading_mode=TradingMode.SPOT ) - Trade.query.session.add(trade) + Trade.session.add(trade) for idx, (order, result) in enumerate(data['orders']): amount = order[1] @@ -2554,18 +2622,18 @@ def test_recalc_trade_from_orders_dca(data) -> None: filled=amount, remaining=0, cost=amount * price, - order_date=arrow.utcnow().shift(hours=-10 + idx).datetime, - order_filled_date=arrow.utcnow().shift(hours=-10 + idx).datetime, + order_date=dt_now() - timedelta(hours=10 + idx), + order_filled_date=dt_now() - timedelta(hours=10 + idx), ) trade.orders.append(order_obj) trade.recalc_trade_from_orders() Trade.commit() - orders1 = Order.query.all() + orders1 = Order.session.scalars(select(Order)).all() assert orders1 assert len(orders1) == idx + 1 - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert len(trade.orders) == idx + 1 if idx < len(data) - 1: @@ -2582,6 +2650,6 @@ def test_recalc_trade_from_orders_dca(data) -> None: assert pytest.approx(trade.close_profit_abs) == data['end_profit'] assert pytest.approx(trade.close_profit) == data['end_profit_ratio'] assert not trade.is_open - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.open_order_id is None diff --git a/tests/persistence/test_trade_fromjson.py b/tests/persistence/test_trade_fromjson.py index 529008e02..22053463d 100644 --- a/tests/persistence/test_trade_fromjson.py +++ b/tests/persistence/test_trade_fromjson.py @@ -50,8 +50,8 @@ def test_trade_fromjson(): "stop_loss_ratio": -0.216, "stop_loss_pct": -21.6, "stoploss_order_id": null, - "stoploss_last_update": null, - "stoploss_last_update_timestamp": null, + "stoploss_last_update": "2022-10-18 09:13:42", + "stoploss_last_update_timestamp": 1666077222000, "initial_stop_loss_abs": 0.1981, "initial_stop_loss_ratio": -0.216, "initial_stop_loss_pct": -21.6, diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 739c3a7ac..bc8fe84f1 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -18,8 +18,8 @@ from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import (create_mock_trades_usdt, get_patched_exchange, get_patched_freqtradebot, - log_has, log_has_re, num_log_has) +from tests.conftest import (EXMS, create_mock_trades_usdt, get_patched_exchange, + get_patched_freqtradebot, log_has, log_has_re, num_log_has) # Exclude RemotePairList from tests. @@ -116,7 +116,7 @@ def static_pl_conf(whitelist_conf): def test_log_cached(mocker, static_pl_conf, markets, tickers): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -139,7 +139,7 @@ def test_log_cached(mocker, static_pl_conf, markets, tickers): def test_load_pairlist_noexist(mocker, markets, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " @@ -150,7 +150,7 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): def test_load_pairlist_verify_multi(mocker, markets_static, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_static)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets_static)) plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) # Call different versions one after the other, should always consider what was passed in # and have no side-effects (therefore the same check multiple times) @@ -166,7 +166,7 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) freqtrade.pairlists.refresh_pairlist() # List ordered by BaseVolume whitelist = ['ETH/BTC', 'TKN/BTC'] @@ -180,7 +180,7 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): def test_refresh_static_pairlist(mocker, markets, static_pl_conf): freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), markets=PropertyMock(return_value=markets), ) @@ -204,7 +204,7 @@ def test_refresh_static_pairlist_noexist(mocker, markets, static_pl_conf, pairs, static_pl_conf['exchange']['pair_whitelist'] += pairs freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), markets=PropertyMock(return_value=markets), ) @@ -221,7 +221,7 @@ def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog): static_pl_conf['exchange']['pair_blacklist'] = ['*/BTC'] freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), markets=PropertyMock(return_value=markets), ) @@ -237,7 +237,7 @@ def test_remove_logs_for_pairs_already_in_blacklist(mocker, markets, static_pl_c logger = logging.getLogger(__name__) freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), markets=PropertyMock(return_value=markets), ) @@ -264,14 +264,14 @@ def test_remove_logs_for_pairs_already_in_blacklist(mocker, markets, static_pl_c def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_tickers=tickers, exchange_has=MagicMock(return_value=True), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) # Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=shitcoinmarkets), ) # argument: use the whitelist dynamically by exchange-volume @@ -291,7 +291,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ tickers_dict = tickers() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), ) # Remove caching of ticker data to emulate changing volume by the time of second call @@ -302,7 +302,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_2) # Remock markets with shitcoinmarkets since get_patched_freqtradebot uses the markets fixture mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=shitcoinmarkets), ) @@ -320,11 +320,11 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, exchange_has=MagicMock(return_value=True), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_empty)) + mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets_empty)) # argument: use the whitelist dynamically by exchange-volume whitelist = [] @@ -523,15 +523,15 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history_high_vola, } - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_tickers=tickers, markets=PropertyMock(return_value=shitcoinmarkets) ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) @@ -649,7 +649,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, ('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history_high_volume, } - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) if volumefilter_result == 'default_refresh_too_short': with pytest.raises(OperationalException, @@ -675,7 +675,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, else: freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_tickers=tickers, markets=PropertyMock(return_value=shitcoinmarkets) ) @@ -687,7 +687,7 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_data = [] mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) @@ -702,7 +702,7 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] del whitelist_conf['stoploss'] - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) with pytest.raises(OperationalException, match=r"PrecisionFilter can only work with stoploss defined\..*"): @@ -711,9 +711,9 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] - if hasattr(Trade, 'query'): - del Trade.query - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + if hasattr(Trade, 'session'): + del Trade.session + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, whitelist_conf) pm = PairListManager(exchange, whitelist_conf, MagicMock()) pm.refresh_pairlist() @@ -755,7 +755,7 @@ def test_PerformanceFilter_lookback(mocker, default_conf_usdt, fee, caplog) -> N {"method": "StaticPairList"}, {"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01} ] - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, default_conf_usdt) pm = PairListManager(exchange, default_conf_usdt) pm.refresh_pairlist() @@ -781,7 +781,7 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog {"method": "StaticPairList", "allow_inactive": True}, {"method": "PerformanceFilter", "minutes": 60, } ] - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf_usdt) pm = PairListManager(exchange, default_conf_usdt) pm.refresh_pairlist() @@ -806,7 +806,7 @@ def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_tickers=tickers, exchange_has=MagicMock(return_value=False), ) @@ -819,7 +819,7 @@ def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'StaticPairList'}, {'method': 'SpreadFilter'}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_tickers=tickers, exchange_has=MagicMock(return_value=False), ) @@ -828,11 +828,17 @@ def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> N match=r'Exchange does not support fetchTickers, .*'): get_patched_freqtradebot(mocker, default_conf) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.get_option', MagicMock(return_value=False)) + with pytest.raises(OperationalException, + match=r'.*requires exchange to have bid/ask data'): + get_patched_freqtradebot(mocker, default_conf) + @pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS) def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): whitelist_conf['pairlists'][0]['method'] = pairlist - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True) ) @@ -861,7 +867,7 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, log_message, tickers): whitelist_conf['pairlists'][0]['method'] = pairlist - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -881,10 +887,10 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, tickers): whitelist_conf['pairlists'][0]['method'] = pairlist - mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + mocker.patch(f'{EXMS}.exchange_has', return_value=True) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=None), get_tickers=tickers ) @@ -897,7 +903,7 @@ def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, t def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf): whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) with pytest.raises(OperationalException, match=r"key asdf not in .*"): get_patched_freqtradebot(mocker, whitelist_conf) @@ -905,7 +911,7 @@ def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf): def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -925,7 +931,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': -1}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -941,7 +947,7 @@ def test_agefilter_max_days_lower_than_min_days(mocker, default_conf, markets, t {'method': 'AgeFilter', 'min_days_listed': 3, "max_days_listed": 2}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -956,7 +962,7 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': 99999}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -976,7 +982,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, } mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers, @@ -1000,14 +1006,14 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history.iloc[[0]], } - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 # Move to next day t.move_to("2021-09-02 01:00:00 +00:00") - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 @@ -1021,7 +1027,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history, } - mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 4 # Called once (only for XRP/BTC) @@ -1033,7 +1039,7 @@ def test_OffsetFilter_error(mocker, whitelist_conf) -> None: [{"method": "StaticPairList"}, {"method": "OffsetFilter", "offset": -1}] ) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) with pytest.raises(OperationalException, match=r'OffsetFilter requires offset to be >= 0'): @@ -1044,7 +1050,7 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'RangeStabilityFilter', 'lookback_days': 99999}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -1074,7 +1080,7 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh 'min_rate_of_change': min_rate_of_change, "max_rate_of_change": max_rate_of_change}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -1088,7 +1094,7 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh ('BLK/BTC', '1d', CandleType.SPOT): ohlcv_history, } mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) @@ -1109,7 +1115,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'SpreadFilter', 'max_spread_ratio': 0.1}] - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), get_tickers=tickers @@ -1123,7 +1129,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo tickers.return_value['ETH/BTC']['ask'] = 0.0 del tickers.return_value['TKN/BTC'] del tickers.return_value['LTC/BTC'] - mocker.patch.multiple('freqtrade.exchange.Exchange', get_tickers=tickers) + mocker.patch.multiple(EXMS, get_tickers=tickers) ftbot.pairlists.refresh_pairlist() assert log_has_re(r'Removed .* invalid ticker data.*', caplog) @@ -1197,7 +1203,7 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True) ) @@ -1214,7 +1220,7 @@ def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) whitelist_conf['pairlists'] = [] @@ -1266,14 +1272,14 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o allowlist_conf['pairlists'] = pairlists allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, allowlist_conf) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_tickers=tickers, markets=PropertyMock(return_value=markets) ) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch.multiple(EXMS, get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), ) mocker.patch.multiple('freqtrade.persistence.Trade', @@ -1371,7 +1377,7 @@ def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected): def test_ProducerPairlist_no_emc(mocker, whitelist_conf): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) whitelist_conf['pairlists'] = [ { @@ -1388,8 +1394,8 @@ def test_ProducerPairlist_no_emc(mocker, whitelist_conf): def test_ProducerPairlist(mocker, whitelist_conf, markets): - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - mocker.patch.multiple('freqtrade.exchange.Exchange', + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), ) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index 0ba9bb746..6e209df60 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -1,10 +1,10 @@ from datetime import datetime, timedelta, timezone -import arrow import pytest from freqtrade.persistence import PairLocks from freqtrade.persistence.models import PairLock +from freqtrade.util import dt_now @pytest.mark.parametrize('use_db', (False, True)) @@ -14,26 +14,26 @@ def test_PairLocks(use_db): PairLocks.use_db = use_db # No lock should be present if use_db: - assert len(PairLock.query.all()) == 0 + assert len(PairLock.get_all_locks().all()) == 0 assert PairLocks.use_db == use_db pair = 'ETH/BTC' assert not PairLocks.is_pair_locked(pair) - PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4)) # ETH/BTC locked for 4 minutes (on both sides) assert PairLocks.is_pair_locked(pair) assert PairLocks.is_pair_locked(pair, side='long') assert PairLocks.is_pair_locked(pair, side='short') pair = 'BNB/BTC' - PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime, side='long') + PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4), side='long') assert not PairLocks.is_pair_locked(pair) assert PairLocks.is_pair_locked(pair, side='long') assert not PairLocks.is_pair_locked(pair, side='short') pair = 'BNB/USDT' - PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime, side='short') + PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4), side='short') assert not PairLocks.is_pair_locked(pair) assert not PairLocks.is_pair_locked(pair, side='long') assert PairLocks.is_pair_locked(pair, side='short') @@ -44,7 +44,7 @@ def test_PairLocks(use_db): # Unlocking a pair that's not locked should not raise an error PairLocks.unlock_pair(pair) - PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4)) assert PairLocks.is_pair_locked(pair) # Get both locks from above @@ -88,13 +88,13 @@ def test_PairLocks(use_db): if use_db: locks = PairLocks.get_all_locks() - locks_db = PairLock.query.all() + locks_db = PairLock.get_all_locks().all() assert len(locks) == len(locks_db) assert len(locks_db) > 0 else: # Nothing was pushed to the database assert len(PairLocks.get_all_locks()) > 0 - assert len(PairLock.query.all()) == 0 + assert len(PairLock.get_all_locks().all()) == 0 # Reset use-db variable PairLocks.reset_locks() PairLocks.use_db = True @@ -107,26 +107,26 @@ def test_PairLocks_getlongestlock(use_db): # No lock should be present PairLocks.use_db = use_db if use_db: - assert len(PairLock.query.all()) == 0 + assert len(PairLock.get_all_locks().all()) == 0 assert PairLocks.use_db == use_db pair = 'ETH/BTC' assert not PairLocks.is_pair_locked(pair) - PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4)) # ETH/BTC locked for 4 minutes assert PairLocks.is_pair_locked(pair) lock = PairLocks.get_pair_longest_lock(pair) - assert lock.lock_end_time.replace(tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=3) - assert lock.lock_end_time.replace(tzinfo=timezone.utc) < arrow.utcnow().shift(minutes=14) + assert lock.lock_end_time.replace(tzinfo=timezone.utc) > dt_now() + timedelta(minutes=3) + assert lock.lock_end_time.replace(tzinfo=timezone.utc) < dt_now() + timedelta(minutes=14) - PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=15).datetime) + PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=15)) assert PairLocks.is_pair_locked(pair) lock = PairLocks.get_pair_longest_lock(pair) # Must be longer than above - assert lock.lock_end_time.replace(tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=14) + assert lock.lock_end_time.replace(tzinfo=timezone.utc) > dt_now() + timedelta(minutes=14) PairLocks.reset_locks() PairLocks.use_db = True @@ -139,12 +139,12 @@ def test_PairLocks_reason(use_db): PairLocks.use_db = use_db # No lock should be present if use_db: - assert len(PairLock.query.all()) == 0 + assert len(PairLock.get_all_locks().all()) == 0 assert PairLocks.use_db == use_db - PairLocks.lock_pair('XRP/USDT', arrow.utcnow().shift(minutes=4).datetime, 'TestLock1') - PairLocks.lock_pair('ETH/USDT', arrow.utcnow().shift(minutes=4).datetime, 'TestLock2') + PairLocks.lock_pair('XRP/USDT', dt_now() + timedelta(minutes=4), 'TestLock1') + PairLocks.lock_pair('ETH/USDT', dt_now() + timedelta(minutes=4), 'TestLock2') assert PairLocks.is_pair_locked('XRP/USDT') assert PairLocks.is_pair_locked('ETH/USDT') diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2bbdf3d4f..8fe8cec6b 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,5 +1,5 @@ import random -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest @@ -24,8 +24,8 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, stake_amount=0.01, fee_open=fee, fee_close=fee, - open_date=datetime.utcnow() - timedelta(minutes=min_ago_open or 200), - close_date=datetime.utcnow() - timedelta(minutes=min_ago_close or 30), + open_date=datetime.now(timezone.utc) - timedelta(minutes=min_ago_open or 200), + close_date=datetime.now(timezone.utc) - timedelta(minutes=min_ago_close or 30), open_rate=open_rate, is_open=is_open, amount=0.01 / open_rate, @@ -74,7 +74,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, trade.close(close_price) trade.exit_reason = exit_reason - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() return trade @@ -87,9 +87,9 @@ def test_protectionmanager(mocker, default_conf): for handler in freqtrade.protections._protection_handlers: assert handler.name in constants.AVAILABLE_PROTECTIONS if not handler.has_global_stop: - assert handler.global_stop(datetime.utcnow(), '*') is None + assert handler.global_stop(datetime.now(timezone.utc), '*') is None if not handler.has_local_stop: - assert handler.stop_per_pair('XRP/BTC', datetime.utcnow(), '*') is None + assert handler.stop_per_pair('XRP/BTC', datetime.now(timezone.utc), '*') is None @pytest.mark.parametrize('timeframe,expected,protconf', [ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4871d9b24..405727d8c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1,12 +1,10 @@ -# pragma pylint: disable=missing-docstring, C0103 -# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments - from copy import deepcopy from datetime import datetime, timedelta, timezone from unittest.mock import ANY, MagicMock, PropertyMock import pytest from numpy import isnan +from sqlalchemy import select from freqtrade.edge import PairInfo from freqtrade.enums import SignalDirection, State, TradingMode @@ -15,19 +13,10 @@ from freqtrade.persistence import Trade from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter -from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot, - patch_get_signal) +from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt, + get_patched_freqtradebot, patch_get_signal) -# Functions for recurrent object patching -def prec_satoshi(a, b) -> float: - """ - :return: True if A and B differs less than one satoshi. - """ - return abs(a - b) < 0.00000001 - - -# Unit tests def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: gen_response = { 'trade_id': 1, @@ -62,15 +51,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'amount': 91.07468123, 'amount_requested': 91.07468124, 'stake_amount': 0.001, - 'max_stake_amount': ANY, + 'max_stake_amount': None, 'trade_duration': None, 'trade_duration_s': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, - 'current_profit': -0.00408133, - 'current_profit_pct': -0.41, - 'current_profit_abs': -4.09e-06, 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, @@ -91,6 +77,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10376381, 'open_order': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, + 'total_profit_abs': -4.09e-06, + 'total_profit_fiat': ANY, + 'total_profit_ratio': None, 'exchange': 'binance', 'leverage': 1.0, 'interest_rate': 0.0, @@ -98,6 +88,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'is_short': False, 'funding_fees': 0.0, 'trading_mode': TradingMode.SPOT, + 'amount_precision': 8.0, + 'price_precision': 8.0, + 'precision_mode': 2, 'orders': [{ 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', @@ -109,10 +102,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: } mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, - _is_dry_limit_order_filled=MagicMock(side_effect=[False, True]), + _dry_is_price_crossed=MagicMock(side_effect=[False, True]), ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -134,20 +127,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': 0.0, 'profit_pct': 0.0, 'profit_abs': 0.0, - 'current_profit': 0.0, - 'current_profit_pct': 0.0, - 'current_profit_abs': 0.0, - 'stop_loss_abs': 0.0, - 'stop_loss_pct': None, - 'stop_loss_ratio': None, - 'stoploss_current_dist': -1.099e-05, - 'stoploss_current_dist_ratio': -1.0, - 'stoploss_current_dist_pct': pytest.approx(-100.0), - 'stoploss_entry_dist': -0.0010025, - 'stoploss_entry_dist_ratio': -1.0, - 'initial_stop_loss_abs': 0.0, - 'initial_stop_loss_pct': None, - 'initial_stop_loss_ratio': None, + 'total_profit_abs': 0.0, 'open_order': '(limit buy rem=91.07468123)', }) response_unfilled['orders'][0].update({ @@ -182,12 +162,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: results = rpc._rpc_trade_status() response = deepcopy(gen_response) + response.update({ + 'max_stake_amount': 0.001, + 'total_profit_ratio': pytest.approx(-0.00409), + }) assert results[0] == response - mocker.patch('freqtrade.exchange.Exchange.get_rate', + mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() - assert isnan(results[0]['current_profit']) + assert isnan(results[0]['profit_ratio']) assert isnan(results[0]['current_rate']) response_norate = deepcopy(gen_response) # Update elements that are NaN when no rate is available. @@ -195,12 +179,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_current_dist': ANY, 'stoploss_current_dist_ratio': ANY, 'stoploss_current_dist_pct': ANY, + 'max_stake_amount': 0.001, 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, - 'current_profit_abs': ANY, - 'current_profit': ANY, - 'current_profit_pct': ANY, + 'total_profit_abs': ANY, + 'total_profit_ratio': ANY, 'current_rate': ANY, }) assert results[0] == response_norate @@ -214,7 +198,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -226,7 +210,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING with pytest.raises(RPCException, match=r'.*no active trade*'): rpc._rpc_status_table(default_conf['stake_currency'], 'USD') - mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) freqtradebot.enter_positions() result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') @@ -237,7 +221,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '0.00' == result[0][3] assert isnan(fiat_profit_sum) - mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True) + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) freqtradebot.process() result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') @@ -248,7 +232,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.41%' == result[0][3] assert isnan(fiat_profit_sum) - # Test with fiatconvert + # Test with fiat convert rpc._fiat_converter = CryptoToFiatConverter() result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers @@ -268,7 +252,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: # 3 on top of the initial one. assert result[0][4] == '1/4' - mocker.patch('freqtrade.exchange.Exchange.get_rate', + mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] @@ -277,11 +261,10 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert isnan(fiat_profit_sum) -def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, - limit_buy_order, limit_sell_order, markets, mocker) -> None: +def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, markets, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) @@ -311,7 +294,7 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, assert day['starting_balance'] in (pytest.approx(1062.37), pytest.approx(1066.46)) assert day['fiat_value'] in (0.0, ) # ensure first day is current date - assert str(days['data'][0]['date']) == str(datetime.utcnow().date()) + assert str(days['data'][0]['date']) == str(datetime.now(timezone.utc).date()) # Try invalid data with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'): @@ -322,7 +305,7 @@ def test__rpc_timeunit_profit(default_conf_usdt, ticker, fee, def test_rpc_trade_history(mocker, default_conf, markets, fee, is_short): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) @@ -350,7 +333,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): stoploss_mock = MagicMock() cancel_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), cancel_order=cancel_mock, cancel_stoploss_order=stoploss_mock, @@ -363,7 +346,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): with pytest.raises(RPCException, match='invalid argument'): rpc._rpc_delete('200') - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() trades[1].stoploss_order_id = '1234' trades[2].stoploss_order_id = '1234' assert len(trades) > 2 @@ -384,15 +367,13 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): assert stoploss_mock.call_count == 1 assert res['cancel_order_count'] == 2 - stoploss_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', - side_effect=InvalidOrderException) + stoploss_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException) res = rpc._rpc_delete('3') assert stoploss_mock.call_count == 1 stoploss_mock.reset_mock() - cancel_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_order', - side_effect=InvalidOrderException) + cancel_mock = mocker.patch(f'{EXMS}.cancel_order', side_effect=InvalidOrderException) res = rpc._rpc_delete('4') assert cancel_mock.call_count == 1 @@ -403,7 +384,7 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -433,19 +414,19 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert pytest.approx(stats['profit_all_percent_mean']) == -57.86 assert pytest.approx(stats['profit_all_fiat']) == -85.205614098 assert stats['trade_count'] == 7 - assert stats['first_trade_date'] == '2 days ago' - assert stats['latest_trade_date'] == '17 minutes ago' + assert stats['first_trade_humanized'] == '2 days ago' + assert stats['latest_trade_humanized'] == '17 minutes ago' assert stats['avg_duration'] in ('0:17:40') assert stats['best_pair'] == 'XRP/USDT' assert stats['best_rate'] == 10.0 # Test non-available pair - mocker.patch('freqtrade.exchange.Exchange.get_rate', + mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'XRP/USDT' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert stats['trade_count'] == 7 - assert stats['first_trade_date'] == '2 days ago' - assert stats['latest_trade_date'] == '17 minutes ago' + assert stats['first_trade_humanized'] == '2 days ago' + assert stats['latest_trade_humanized'] == '17 minutes ago' assert stats['avg_duration'] in ('0:17:40') assert stats['best_pair'] == 'XRP/USDT' assert stats['best_rate'] == 10.0 @@ -474,7 +455,7 @@ def test_rpc_balance_handle_error(default_conf, mocker): mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=mock_balance), get_tickers=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx')) ) @@ -537,7 +518,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, validate_trading_mode_and_margin_mode=MagicMock(), get_balances=MagicMock(return_value=mock_balance), fetch_positions=MagicMock(return_value=mock_pos), @@ -553,8 +534,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): rpc._fiat_converter = CryptoToFiatConverter() result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) - assert prec_satoshi(result['total'], 30.30909624) - assert prec_satoshi(result['value'], 454636.44360691) + assert pytest.approx(result['total']) == 30.30909624 + assert pytest.approx(result['value']) == 454636.44360691 assert tickers.call_count == 1 assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] @@ -564,57 +545,73 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'free': 10.0, 'balance': 12.0, 'used': 2.0, + 'bot_owned': 9.9, # available stake - reducing by reserved amount 'est_stake': 10.0, # In futures mode, "free" is used here. + 'est_stake_bot': 9.9, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, }, { 'free': 1.0, 'balance': 5.0, 'currency': 'ETH', + 'bot_owned': 0, 'est_stake': 0.30794, + 'est_stake_bot': 0, 'used': 4.0, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, 'position': 0.0, 'side': 'long', - + 'is_bot_managed': False, }, { 'free': 5.0, 'balance': 10.0, 'currency': 'USDT', + 'bot_owned': 0, 'est_stake': 0.0011562404610161968, + 'est_stake_bot': 0, 'used': 5.0, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': False, }, { 'free': 0.0, 'balance': 0.0, 'currency': 'ETH/USDT:USDT', 'est_stake': 20, + 'est_stake_bot': 20, 'used': 0, 'stake': 'BTC', 'is_position': True, 'leverage': 5.0, 'position': 1000.0, 'side': 'short', + 'is_bot_managed': True, } ] + assert pytest.approx(result['total_bot']) == 29.9 + assert pytest.approx(result['total']) == 30.309096 + assert result['starting_capital'] == 10 + # Very high starting capital ratio, because the futures position really has the wrong unit. + # TODO: improve this test (see comment above) + assert result['starting_capital_ratio'] == pytest.approx(1.98999999) def test_rpc_start(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock() ) @@ -635,7 +632,7 @@ def test_rpc_start(mocker, default_conf) -> None: def test_rpc_stop(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock() ) @@ -657,7 +654,7 @@ def test_rpc_stop(mocker, default_conf) -> None: def test_rpc_stopentry(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=MagicMock() ) @@ -677,7 +674,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: cancel_order_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, cancel_order=cancel_order_mock, fetch_order=MagicMock( @@ -688,7 +685,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: 'filled': 0.0, } ), - _is_dry_limit_order_filled=MagicMock(return_value=True), + _dry_is_price_crossed=MagicMock(return_value=True), get_fee=fee, ) mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000) @@ -725,15 +722,14 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING assert cancel_order_mock.call_count == 0 - mocker.patch( - 'freqtrade.exchange.Exchange._is_dry_limit_order_filled', MagicMock(return_value=False)) + mocker.patch(f'{EXMS}._dry_is_price_crossed', MagicMock(return_value=False)) freqtradebot.enter_positions() # make an limit-buy open trade - trade = Trade.query.filter(Trade.id == '3').first() + trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first() filled_amount = trade.amount / 2 # Fetch order - it's open first, and closed after cancel_order is called. mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', side_effect=[{ 'id': trade.orders[0].order_id, 'status': 'open', @@ -755,7 +751,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: assert pytest.approx(trade.amount) == filled_amount mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -765,11 +761,11 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: freqtradebot.config['max_open_trades'] = 3 freqtradebot.enter_positions() - trade = Trade.query.filter(Trade.id == '2').first() + trade = Trade.session.scalars(select(Trade).filter(Trade.id == '2')).first() amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -783,11 +779,11 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 2 assert trade.amount == amount - trade = Trade.query.filter(Trade.id == '3').first() + trade = Trade.session.scalars(select(Trade).filter(Trade.id == '3')).first() # make an limit-sell open trade mocker.patch( - 'freqtrade.exchange.Exchange.fetch_order', + f'{EXMS}.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -807,7 +803,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -830,7 +826,7 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -862,7 +858,7 @@ def test_enter_tag_performance_handle(default_conf, ticker, fee, mocker) -> None def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) @@ -875,23 +871,23 @@ def test_enter_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['enter_tag'] == 'TEST1' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 assert res[1]['enter_tag'] == 'Other' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit_pct'], 1.0) + assert pytest.approx(res[1]['profit_pct']) == 1.0 # Test for a specific pair res = rpc._rpc_enter_tag_performance('ETC/BTC') assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['enter_tag'] == 'TEST1' - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -918,7 +914,7 @@ def test_exit_reason_performance_handle(default_conf_usdt, ticker, fee, mocker) def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) @@ -931,23 +927,23 @@ def test_exit_reason_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['exit_reason'] == 'sell_signal' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 assert res[1]['exit_reason'] == 'roi' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit_pct'], 1.0) + assert pytest.approx(res[1]['profit_pct']) == 1.0 # Test for a specific pair res = rpc._rpc_exit_reason_performance('ETC/BTC') assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['exit_reason'] == 'sell_signal' - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -971,7 +967,7 @@ def test_mix_tag_performance_handle(default_conf, ticker, fee, mocker) -> None: def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) @@ -984,10 +980,10 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 2 assert res[0]['mix_tag'] == 'TEST1 sell_signal' assert res[0]['count'] == 1 - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 assert res[1]['mix_tag'] == 'Other roi' assert res[1]['count'] == 1 - assert prec_satoshi(res[1]['profit_pct'], 1.0) + assert pytest.approx(res[1]['profit_pct']) == 1.0 # Test for a specific pair res = rpc._rpc_mix_tag_performance('ETC/BTC') @@ -995,13 +991,13 @@ def test_mix_tag_performance_handle_2(mocker, default_conf, markets, fee): assert len(res) == 1 assert res[0]['count'] == 1 assert res[0]['mix_tag'] == 'TEST1 sell_signal' - assert prec_satoshi(res[0]['profit_pct'], 0.5) + assert pytest.approx(res[0]['profit_pct']) == 0.5 def test_rpc_count(mocker, default_conf, ticker, fee) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -1026,7 +1022,7 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) buy_mm = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -1155,7 +1151,7 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4, }] - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -1252,6 +1248,6 @@ def test_rpc_health(mocker, default_conf) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) - result = rpc._health() - assert result['last_process'] == '1970-01-01 00:00:00+00:00' - assert result['last_process_ts'] == 0 + result = rpc.health() + assert result['last_process'] is None + assert result['last_process_ts'] is None diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index dd5521f97..842981ad0 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1,6 +1,7 @@ """ Unit test file for rpc/api_server.py """ +import asyncio import logging import time from datetime import datetime, timedelta, timezone @@ -14,17 +15,20 @@ from fastapi import FastAPI, WebSocketDisconnect from fastapi.exceptions import HTTPException from fastapi.testclient import TestClient from requests.auth import _basic_auth_str +from sqlalchemy import select from freqtrade.__init__ import __version__ from freqtrade.enums import CandleType, RunMode, State, TradingMode from freqtrade.exceptions import DependencyException, ExchangeError, OperationalException from freqtrade.loggers import setup_logging, setup_logging_pre +from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer -from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_mock_coro, +from freqtrade.rpc.api_server.webserver_bgwork import ApiBG +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) @@ -281,7 +285,7 @@ def test_api__init__(default_conf, mocker): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') mocker.patch('freqtrade.rpc.api_server.webserver.ApiServer.start_api', MagicMock()) apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) @@ -298,10 +302,6 @@ def test_api_UvicornServer(mocker): s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert thread_mock.call_count == 0 - s.install_signal_handlers() - # Original implementation starts a thread - make sure that's not the case - assert thread_mock.call_count == 0 - # Fake started to avoid sleeping forever s.started = True s.run_in_thread() @@ -317,10 +317,6 @@ def test_api_UvicornServer_run(mocker): s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert serve_mock.call_count == 0 - s.install_signal_handlers() - # Original implementation starts a thread - make sure that's not the case - assert serve_mock.call_count == 0 - # Fake started to avoid sleeping forever s.started = True s.run() @@ -330,13 +326,10 @@ def test_api_UvicornServer_run(mocker): def test_api_UvicornServer_run_no_uvloop(mocker, import_fails): serve_mock = mocker.patch('freqtrade.rpc.api_server.uvicorn_threaded.UvicornServer.serve', get_mock_coro(None)) + asyncio.set_event_loop(asyncio.new_event_loop()) s = UvicornServer(uvicorn.Config(MagicMock(), port=8080, host='127.0.0.1')) assert serve_mock.call_count == 0 - s.install_signal_handlers() - # Original implementation starts a thread - make sure that's not the case - assert serve_mock.call_count == 0 - # Fake started to avoid sleeping forever s.started = True s.run() @@ -350,7 +343,7 @@ def test_api_run(default_conf, mocker, caplog): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') server_inst_mock = MagicMock() server_inst_mock.run_in_thread = MagicMock() @@ -428,7 +421,7 @@ def test_api_cleanup(default_conf, mocker, caplog): "username": "TestUser", "password": "testPass", }}) - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init') server_mock = MagicMock() server_mock.cleanup = MagicMock() @@ -473,9 +466,9 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): ftbot, client = botclient ftbot.config['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', + mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance) + mocker.patch(f'{EXMS}.get_tickers', tickers) + mocker.patch(f'{EXMS}.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") ftbot.wallets.update() @@ -489,13 +482,18 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): 'free': 12.0, 'balance': 12.0, 'used': 0.0, + 'bot_owned': pytest.approx(11.879999), 'est_stake': 12.0, + 'est_stake_bot': pytest.approx(11.879999), 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, } + assert response['total'] == 12.159513094 + assert response['total_bot'] == pytest.approx(11.879999) assert 'starting_capital' in response assert 'starting_capital_fiat' in response assert 'starting_capital_pct' in response @@ -507,7 +505,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -594,7 +592,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -605,7 +603,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): assert len(rc.json()['data']) == 7 assert rc.json()['stake_currency'] == 'BTC' assert rc.json()['fiat_display_currency'] == 'USD' - assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date()) + assert rc.json()['data'][0]['date'] == str(datetime.now(timezone.utc).date()) @pytest.mark.parametrize('is_short', [True, False]) @@ -613,7 +611,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets) ) rc = client_get(client, f"{BASE_URI}/trades") @@ -624,7 +622,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short): assert rc.json()['offset'] == 0 create_mock_trades(fee, is_short=is_short) - Trade.query.session.flush() + Trade.session.flush() rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) @@ -644,7 +642,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), fetch_ticker=ticker, ) @@ -652,7 +650,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short): assert_response(rc, 404) assert rc.json()['detail'] == 'Trade not found.' - Trade.query.session.rollback() + Trade.rollback() create_mock_trades(fee, is_short=is_short) rc = client_get(client, f"{BASE_URI}/trade/3") @@ -668,7 +666,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): stoploss_mock = MagicMock() cancel_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, markets=PropertyMock(return_value=markets), cancel_order=cancel_mock, cancel_stoploss_order=stoploss_mock, @@ -677,7 +675,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): create_mock_trades(fee, is_short=is_short) ftbot.strategy.order_types['stoploss_on_exchange'] = True - trades = Trade.query.all() + trades = Trade.session.scalars(select(Trade)).all() trades[1].stoploss_order_id = '1234' Trade.commit() assert len(trades) > 2 @@ -685,7 +683,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): rc = client_delete(client, f"{BASE_URI}/trades/1") assert_response(rc) assert rc.json()['result_msg'] == 'Deleted trade 1. Closed 1 open orders.' - assert len(trades) - 1 == len(Trade.query.all()) + assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all()) assert cancel_mock.call_count == 1 cancel_mock.reset_mock() @@ -694,11 +692,11 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): assert_response(rc, 502) assert cancel_mock.call_count == 0 - assert len(trades) - 1 == len(Trade.query.all()) + assert len(trades) - 1 == len(Trade.session.scalars(select(Trade)).all()) rc = client_delete(client, f"{BASE_URI}/trades/2") assert_response(rc) assert rc.json()['result_msg'] == 'Deleted trade 2. Closed 2 open orders.' - assert len(trades) - 2 == len(Trade.query.all()) + assert len(trades) - 2 == len(Trade.session.scalars(select(Trade)).all()) assert stoploss_mock.call_count == 1 rc = client_delete(client, f"{BASE_URI}/trades/502") @@ -706,6 +704,71 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): assert_response(rc, 502) +@pytest.mark.parametrize('is_short', [True, False]) +def test_api_delete_open_order(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() + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + fetch_ticker=ticker, + cancel_order=cancel_mock, + cancel_stoploss_order=stoploss_mock, + ) + + rc = client_delete(client, f"{BASE_URI}/trades/10/open-order") + assert_response(rc, 502) + assert 'Invalid trade_id.' in rc.json()['error'] + + create_mock_trades(fee, is_short=is_short) + Trade.commit() + + rc = client_delete(client, f"{BASE_URI}/trades/5/open-order") + assert_response(rc, 502) + assert 'No open order for trade_id' in rc.json()['error'] + trade = Trade.get_trades([Trade.id == 6]).first() + mocker.patch(f'{EXMS}.fetch_order', side_effect=ExchangeError) + rc = client_delete(client, f"{BASE_URI}/trades/6/open-order") + assert_response(rc, 502) + assert 'Order not found.' in rc.json()['error'] + + trade = Trade.get_trades([Trade.id == 6]).first() + mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object()) + + rc = client_delete(client, f"{BASE_URI}/trades/6/open-order") + assert_response(rc) + 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_post(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_post(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") @@ -742,7 +805,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -804,7 +867,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -827,8 +890,10 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) 'best_pair_profit_ratio': expected['best_pair_profit_ratio'], 'best_rate': expected['best_rate'], 'first_trade_date': ANY, + 'first_trade_humanized': ANY, 'first_trade_timestamp': ANY, - 'latest_trade_date': '5 minutes ago', + 'latest_trade_date': ANY, + 'latest_trade_humanized': '5 minutes ago', 'latest_trade_timestamp': ANY, 'profit_all_coin': pytest.approx(expected['profit_all_coin']), 'profit_all_fiat': pytest.approx(expected['profit_all_fiat']), @@ -854,6 +919,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) 'max_drawdown': ANY, 'max_drawdown_abs': ANY, 'trading_volume': expected['trading_volume'], + 'bot_start_timestamp': 0, + 'bot_start_date': '', } @@ -862,7 +929,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -905,7 +972,7 @@ def test_api_performance(botclient, fee): ) trade.close_profit = trade.calc_profit_ratio(trade.close_rate) trade.close_profit_abs = trade.calc_profit(trade.close_rate) - Trade.query.session.add(trade) + Trade.session.add(trade) trade = Trade( pair='XRP/ETH', @@ -922,7 +989,7 @@ def test_api_performance(botclient, fee): trade.close_profit = trade.calc_profit_ratio(trade.close_rate) trade.close_profit_abs = trade.calc_profit(trade.close_rate) - Trade.query.session.add(trade) + Trade.session.add(trade) Trade.commit() rc = client_get(client, f"{BASE_URI}/performance") @@ -943,7 +1010,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, @@ -968,13 +1035,15 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'close_profit_pct': None, 'close_profit_abs': None, 'close_rate': None, - 'current_profit': ANY, - 'current_profit_pct': ANY, - 'current_profit_abs': ANY, 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, 'profit_fiat': ANY, + 'total_profit_abs': ANY, + 'total_profit_fiat': ANY, + 'total_profit_ratio': ANY, + 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'current_rate': current_rate, 'open_date': ANY, 'open_timestamp': ANY, @@ -1025,10 +1094,13 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'liquidation_price': None, 'funding_fees': None, 'trading_mode': ANY, + 'amount_precision': None, + 'price_precision': None, + 'precision_mode': None, 'orders': [ANY], } - mocker.patch('freqtrade.exchange.Exchange.get_rate', + mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) rc = client_get(client, f"{BASE_URI}/status") @@ -1141,7 +1213,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): ftbot.config['force_entry_enable'] = True fbuy_mock = MagicMock(return_value=None) - mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) + mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock) rc = client_post(client, f"{BASE_URI}/{endpoint}", data={"pair": "ETH/BTC"}) assert_response(rc) @@ -1156,7 +1228,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): stake_amount=1, open_rate=0.245441, open_order_id="123456", - open_date=datetime.utcnow(), + open_date=datetime.now(timezone.utc), is_open=False, is_short=False, fee_close=fee.return_value, @@ -1167,7 +1239,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): strategy=CURRENT_TEST_STRATEGY, trading_mode=TradingMode.SPOT )) - mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) + mocker.patch("freqtrade.rpc.rpc.RPC._rpc_force_entry", fbuy_mock) rc = client_post(client, f"{BASE_URI}/{endpoint}", data={"pair": "ETH/BTC"}) @@ -1204,6 +1276,8 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'profit_pct': None, 'profit_abs': None, 'profit_fiat': None, + 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, @@ -1228,6 +1302,9 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'liquidation_price': None, 'funding_fees': None, 'trading_mode': 'spot', + 'amount_precision': None, + 'price_precision': None, + 'precision_mode': None, 'orders': [], } @@ -1235,12 +1312,12 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): def test_api_forceexit(botclient, mocker, ticker, fee, markets): ftbot, client = botclient mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets), - _is_dry_limit_order_filled=MagicMock(return_value=True), + _dry_is_price_crossed=MagicMock(return_value=True), ) patch_get_signal(ftbot) @@ -1248,7 +1325,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets): data={"tradeid": "1"}) assert_response(rc, 502) assert rc.json() == {"error": "Error querying /api/v1/forceexit: invalid argument"} - Trade.query.session.rollback() + Trade.rollback() create_mock_trades(fee) trade = Trade.get_trades([Trade.id == 5]).first() @@ -1257,7 +1334,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets): data={"tradeid": "5", "ordertype": "market", "amount": 23}) assert_response(rc) assert rc.json() == {'result': 'Created sell order for trade 5.'} - Trade.query.session.rollback() + Trade.rollback() trade = Trade.get_trades([Trade.id == 5]).first() assert pytest.approx(trade.amount) == 100 @@ -1267,7 +1344,7 @@ def test_api_forceexit(botclient, mocker, ticker, fee, markets): data={"tradeid": "5"}) assert_response(rc) assert rc.json() == {'result': 'Created sell order for trade 5.'} - Trade.query.session.rollback() + Trade.rollback() trade = Trade.get_trades([Trade.id == 5]).first() assert trade.is_open is False @@ -1364,10 +1441,10 @@ def test_api_pair_candles(botclient, ohlcv_history): ]) -def test_api_pair_history(botclient, ohlcv_history): +def test_api_pair_history(botclient, mocker): ftbot, client = botclient timeframe = '5m' - + lfm = mocker.patch('freqtrade.strategy.interface.IStrategy.load_freqAI_model') # No pair rc = client_get(client, f"{BASE_URI}/pair_history?timeframe={timeframe}" @@ -1401,6 +1478,7 @@ def test_api_pair_history(botclient, ohlcv_history): assert len(rc.json()['data']) == rc.json()['length'] assert 'columns' in rc.json() assert 'data' in rc.json() + assert lfm.call_count == 1 assert rc.json()['pair'] == 'UNITTEST/BTC' assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' @@ -1417,7 +1495,7 @@ def test_api_pair_history(botclient, ohlcv_history): "No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") -def test_api_plot_config(botclient): +def test_api_plot_config(botclient, mocker): ftbot, client = botclient rc = client_get(client, f"{BASE_URI}/plot_config") @@ -1441,6 +1519,21 @@ def test_api_plot_config(botclient): assert isinstance(rc.json()['main_plot'], dict) assert isinstance(rc.json()['subplots'], dict) + rc = client_get(client, f"{BASE_URI}/plot_config?strategy=freqai_test_classifier") + assert_response(rc) + res = rc.json() + assert 'target_roi' in res['subplots'] + assert 'do_predict' in res['subplots'] + + rc = client_get(client, f"{BASE_URI}/plot_config?strategy=HyperoptableStrategy") + assert_response(rc) + assert rc.json()['subplots'] == {} + + mocker.patch('freqtrade.rpc.api_server.api_v1.get_rpc_optional', return_value=None) + + rc = client_get(client, f"{BASE_URI}/plot_config") + assert_response(rc) + def test_api_strategies(botclient, tmpdir): ftbot, client = botclient @@ -1553,13 +1646,13 @@ def test_list_available_pairs(botclient): client, f"{BASE_URI}/available_pairs?timeframe=1h") assert_response(rc) assert rc.json()['length'] == 1 - assert rc.json()['pairs'] == ['XRP/USDT'] + assert rc.json()['pairs'] == ['XRP/USDT:USDT'] rc = client_get( client, f"{BASE_URI}/available_pairs?timeframe=1h&candletype=mark") assert_response(rc) assert rc.json()['length'] == 2 - assert rc.json()['pairs'] == ['UNITTEST/USDT', 'XRP/USDT'] + assert rc.json()['pairs'] == ['UNITTEST/USDT:USDT', 'XRP/USDT:USDT'] assert len(rc.json()['pair_interval']) == 2 @@ -1574,131 +1667,140 @@ def test_sysinfo(botclient): def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): - ftbot, client = botclient - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + try: + ftbot, client = botclient + mocker.patch(f'{EXMS}.get_fee', fee) - rc = client_get(client, f"{BASE_URI}/backtest") - # Backtest prevented in default mode - assert_response(rc, 502) + rc = client_get(client, f"{BASE_URI}/backtest") + # Backtest prevented in default mode + assert_response(rc, 502) - ftbot.config['runmode'] = RunMode.WEBSERVER - # Backtesting not started yet - rc = client_get(client, f"{BASE_URI}/backtest") - assert_response(rc) + ftbot.config['runmode'] = RunMode.WEBSERVER + # Backtesting not started yet + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) - result = rc.json() - assert result['status'] == 'not_started' - assert not result['running'] - assert result['status_msg'] == 'Backtest not yet executed' - assert result['progress'] == 0 + result = rc.json() + assert result['status'] == 'not_started' + assert not result['running'] + assert result['status_msg'] == 'Backtest not yet executed' + assert result['progress'] == 0 - # Reset backtesting - rc = client_delete(client, f"{BASE_URI}/backtest") - assert_response(rc) - result = rc.json() - assert result['status'] == 'reset' - assert not result['running'] - assert result['status_msg'] == 'Backtest reset' - ftbot.config['export'] = 'trades' - ftbot.config['backtest_cache'] = 'day' - ftbot.config['user_data_dir'] = Path(tmpdir) - ftbot.config['exportfilename'] = Path(tmpdir) / "backtest_results" - ftbot.config['exportfilename'].mkdir() + # Reset backtesting + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'reset' + assert not result['running'] + assert result['status_msg'] == 'Backtest reset' + ftbot.config['export'] = 'trades' + ftbot.config['backtest_cache'] = 'day' + ftbot.config['user_data_dir'] = Path(tmpdir) + ftbot.config['exportfilename'] = Path(tmpdir) / "backtest_results" + ftbot.config['exportfilename'].mkdir() - # start backtesting - data = { - "strategy": CURRENT_TEST_STRATEGY, - "timeframe": "5m", - "timerange": "20180110-20180111", - "max_open_trades": 3, - "stake_amount": 100, - "dry_run_wallet": 1000, - "enable_protections": False - } - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert_response(rc) - result = rc.json() + # start backtesting + data = { + "strategy": CURRENT_TEST_STRATEGY, + "timeframe": "5m", + "timerange": "20180110-20180111", + "max_open_trades": 3, + "stake_amount": 100, + "dry_run_wallet": 1000, + "enable_protections": False + } + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert_response(rc) + result = rc.json() - assert result['status'] == 'running' - assert result['progress'] == 0 - assert result['running'] - assert result['status_msg'] == 'Backtest started' + assert result['status'] == 'running' + assert result['progress'] == 0 + assert result['running'] + assert result['status_msg'] == 'Backtest started' - rc = client_get(client, f"{BASE_URI}/backtest") - assert_response(rc) + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) - result = rc.json() - assert result['status'] == 'ended' - assert not result['running'] - assert result['status_msg'] == 'Backtest ended' - assert result['progress'] == 1 - assert result['backtest_result'] + result = rc.json() + assert result['status'] == 'ended' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' + assert result['progress'] == 1 + assert result['backtest_result'] - rc = client_get(client, f"{BASE_URI}/backtest/abort") - assert_response(rc) - result = rc.json() - assert result['status'] == 'not_running' - assert not result['running'] - assert result['status_msg'] == 'Backtest ended' + rc = client_get(client, f"{BASE_URI}/backtest/abort") + assert_response(rc) + result = rc.json() + assert result['status'] == 'not_running' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' - # Simulate running backtest - ApiServer._bgtask_running = True - rc = client_get(client, f"{BASE_URI}/backtest/abort") - assert_response(rc) - result = rc.json() - assert result['status'] == 'stopping' - assert not result['running'] - assert result['status_msg'] == 'Backtest ended' + # Simulate running backtest + ApiBG.bgtask_running = True + rc = client_get(client, f"{BASE_URI}/backtest/abort") + assert_response(rc) + result = rc.json() + assert result['status'] == 'stopping' + assert not result['running'] + assert result['status_msg'] == 'Backtest ended' - # Get running backtest... - rc = client_get(client, f"{BASE_URI}/backtest") - assert_response(rc) - result = rc.json() - assert result['status'] == 'running' - assert result['running'] - assert result['step'] == "backtest" - assert result['status_msg'] == "Backtest running" + # Get running backtest... + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'running' + assert result['running'] + assert result['step'] == "backtest" + assert result['status_msg'] == "Backtest running" - # Try delete with task still running - rc = client_delete(client, f"{BASE_URI}/backtest") - assert_response(rc) - result = rc.json() - assert result['status'] == 'running' + # Try delete with task still running + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'running' - # Post to backtest that's still running - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert_response(rc, 502) - result = rc.json() - assert 'Bot Background task already running' in result['error'] + # Post to backtest that's still running + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert_response(rc, 502) + result = rc.json() + assert 'Bot Background task already running' in result['error'] - ApiServer._bgtask_running = False + ApiBG.bgtask_running = False - # Rerun backtest (should get previous result) - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert_response(rc) - result = rc.json() - assert log_has_re('Reusing result of previous backtest.*', caplog) + # Rerun backtest (should get previous result) + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert_response(rc) + result = rc.json() + assert log_has_re('Reusing result of previous backtest.*', caplog) - data['stake_amount'] = 101 + data['stake_amount'] = 101 - mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy', - side_effect=DependencyException()) - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert log_has("Backtesting caused an error: ", caplog) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest_one_strategy', + side_effect=DependencyException('DeadBeef')) + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert log_has("Backtesting caused an error: DeadBeef", caplog) - # Delete backtesting to avoid leakage since the backtest-object may stick around. - rc = client_delete(client, f"{BASE_URI}/backtest") - assert_response(rc) + rc = client_get(client, f"{BASE_URI}/backtest") + assert_response(rc) + result = rc.json() + assert result['status'] == 'error' + assert 'Backtest failed' in result['status_msg'] - result = rc.json() - assert result['status'] == 'reset' - assert not result['running'] - assert result['status_msg'] == 'Backtest reset' + # Delete backtesting to avoid leakage since the backtest-object may stick around. + rc = client_delete(client, f"{BASE_URI}/backtest") + assert_response(rc) - # Disallow base64 strategies - data['strategy'] = "xx:cHJpbnQoImhlbGxvIHdvcmxkIik=" - rc = client_post(client, f"{BASE_URI}/backtest", data=data) - assert_response(rc, 500) + result = rc.json() + assert result['status'] == 'reset' + assert not result['running'] + assert result['status_msg'] == 'Backtest reset' + + # Disallow base64 strategies + data['strategy'] = "xx:cHJpbnQoImhlbGxvIHdvcmxkIik=" + rc = client_post(client, f"{BASE_URI}/backtest", data=data) + assert_response(rc, 500) + finally: + Backtesting.cleanup() def test_api_backtest_history(botclient, mocker, testdatadir): @@ -1740,8 +1842,8 @@ def test_health(botclient): assert_response(rc) ret = rc.json() - assert ret['last_process_ts'] == 0 - assert ret['last_process'] == '1970-01-01T00:00:00+00:00' + assert ret["last_process_ts"] is None + assert ret["last_process"] is None def test_api_ws_subscribe(botclient, mocker): @@ -1809,7 +1911,7 @@ def test_api_ws_send_msg(default_conf, mocker, caplog): "password": _TEST_PASS, "ws_token": _TEST_WS_TOKEN }}) - mocker.patch('freqtrade.rpc.telegram.Updater') + mocker.patch('freqtrade.rpc.telegram.Telegram._init') mocker.patch('freqtrade.rpc.api_server.ApiServer.start_api') apiserver = ApiServer(default_conf) apiserver.add_rpc_handler(RPC(get_patched_freqtradebot(mocker, default_conf))) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 21c8b0813..f0bb72fc9 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -28,6 +28,7 @@ def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) + default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) @@ -52,6 +53,7 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) + default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) @@ -85,7 +87,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', side_effect=ValueError()) - + default_conf['telegram']['enabled'] = True freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ @@ -99,6 +101,7 @@ def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None: def test_process_msg_queue(mocker, default_conf, caplog) -> None: telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + default_conf['telegram']['enabled'] = True default_conf['telegram']['allow_custom_messages'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init') @@ -115,9 +118,9 @@ def test_process_msg_queue(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: + default_conf['telegram']['enabled'] = True telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') mocker.patch('freqtrade.rpc.telegram.Telegram._init') - freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ @@ -166,7 +169,8 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non caplog) -def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: +def test_startupmessages_telegram_enabled(mocker, default_conf) -> None: + default_conf['telegram']['enabled'] = True telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 85475ae8e..51879f5ad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2,25 +2,28 @@ # pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=too-many-lines, too-many-arguments +import asyncio import logging import re +import threading from datetime import datetime, timedelta, timezone from functools import reduce from random import choice, randint from string import ascii_uppercase -from unittest.mock import ANY, MagicMock +from unittest.mock import ANY, AsyncMock, MagicMock -import arrow import pytest import time_machine from pandas import DataFrame +from sqlalchemy import select from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import BadRequest, NetworkError, TelegramError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo -from freqtrade.enums import ExitType, RPCMessageType, RunMode, SignalDirection, State +from freqtrade.enums import (ExitType, MarketDirection, RPCMessageType, RunMode, SignalDirection, + State) from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging @@ -29,9 +32,44 @@ from freqtrade.persistence.models import Order from freqtrade.rpc import RPC from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.telegram import Telegram, authorized_only -from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, create_mock_trades_usdt, - get_patched_freqtradebot, log_has, log_has_re, patch_exchange, - patch_get_signal, patch_whitelist) +from freqtrade.util.datetime_helpers import dt_now +from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, + create_mock_trades_usdt, get_patched_freqtradebot, log_has, log_has_re, + patch_exchange, patch_get_signal, patch_whitelist) + + +@pytest.fixture(autouse=True) +def mock_exchange_loop(mocker): + mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop') + + +@pytest.fixture +def default_conf(default_conf) -> dict: + # Telegram is enabled by default + default_conf['telegram']['enabled'] = True + return default_conf + + +@pytest.fixture +def update(): + message = Message(0, datetime.now(timezone.utc), Chat(0, 0)) + _update = Update(0, message=message) + + return _update + + +def patch_eventloop_threading(telegrambot): + is_init = False + + def thread_fuck(): + nonlocal is_init + telegrambot._loop = asyncio.new_event_loop() + is_init = True + telegrambot._loop.run_forever() + x = threading.Thread(target=thread_fuck, daemon=True) + x.start() + while not is_init: + pass class DummyCls(Telegram): @@ -47,14 +85,14 @@ class DummyCls(Telegram): pass @authorized_only - def dummy_handler(self, *args, **kwargs) -> None: + async def dummy_handler(self, *args, **kwargs) -> None: """ Fake method that only change the state of the object """ self.state['called'] = True @authorized_only - def dummy_exception(self, *args, **kwargs) -> None: + async def dummy_exception(self, *args, **kwargs) -> None: """ Fake method that throw an exception """ @@ -62,23 +100,26 @@ class DummyCls(Telegram): def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): - msg_mock = MagicMock() + msg_mock = AsyncMock() if mock: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _send_msg=msg_mock + _send_msg=msg_mock, + _start_thread=MagicMock(), ) if not ftbot: + mocker.patch('freqtrade.exchange.exchange.Exchange._init_async_loop') ftbot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(ftbot) telegram = Telegram(rpc, default_conf) + telegram._loop = MagicMock() + patch_eventloop_threading(telegram) return telegram, ftbot, msg_mock def test_telegram__init__(default_conf, mocker) -> None: - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram, _, _ = get_telegram_testobject(mocker, default_conf) @@ -86,43 +127,73 @@ def test_telegram__init__(default_conf, mocker) -> None: def test_telegram_init(default_conf, mocker, caplog) -> None: - start_polling = MagicMock() - mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) + app_mock = MagicMock() + mocker.patch('freqtrade.rpc.telegram.Telegram._start_thread', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init_telegram_app', return_value=app_mock) + mocker.patch('freqtrade.rpc.telegram.Telegram._startup_telegram', AsyncMock()) - get_telegram_testobject(mocker, default_conf, mock=False) - assert start_polling.call_count == 0 + telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) + telegram._init() + assert app_mock.call_count == 0 # number of handles registered - assert start_polling.dispatcher.add_handler.call_count > 0 - assert start_polling.start_polling.call_count == 1 + assert app_mock.add_handler.call_count > 0 + # assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], " - "['forcesell', 'forceexit', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " - "['trades'], ['delete'], ['performance'], " - "['buys', 'entries'], ['sells', 'exits'], ['mix_tags'], " + "['forceexit', 'forcesell', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], " + "['reload_trade'], ['trades'], ['delete'], ['cancel_open_order', 'coo'], " + "['performance'], ['buys', 'entries'], ['exits', 'sells'], ['mix_tags'], " "['stats'], ['daily'], ['weekly'], ['monthly'], " - "['count'], ['locks'], ['unlock', 'delete_locks'], " - "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " + "['count'], ['locks'], ['delete_locks', 'unlock'], " + "['reload_conf', 'reload_config'], ['show_conf', 'show_config'], " "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " - "['blacklist_delete', 'bl_delete'], " - "['logs'], ['edge'], ['health'], ['help'], ['version']" + "['bl_delete', 'blacklist_delete'], " + "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir']" "]") assert log_has(message_str, caplog) -def test_cleanup(default_conf, mocker, ) -> None: +async def test_telegram_startup(default_conf, mocker) -> None: + app_mock = MagicMock() + app_mock.initialize = AsyncMock() + app_mock.start = AsyncMock() + app_mock.updater.start_polling = AsyncMock() + app_mock.updater.running = False + sleep_mock = mocker.patch('freqtrade.rpc.telegram.asyncio.sleep', AsyncMock()) + + telegram, _, _ = get_telegram_testobject(mocker, default_conf) + telegram._app = app_mock + await telegram._startup_telegram() + assert app_mock.initialize.call_count == 1 + assert app_mock.start.call_count == 1 + assert app_mock.updater.start_polling.call_count == 1 + assert sleep_mock.call_count == 1 + + +async def test_telegram_cleanup(default_conf, mocker, ) -> None: + app_mock = MagicMock() + app_mock.stop = AsyncMock() + app_mock.initialize = AsyncMock() + updater_mock = MagicMock() - updater_mock.stop = MagicMock() - mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) + updater_mock.stop = AsyncMock() + app_mock.updater = updater_mock + # mocker.patch('freqtrade.rpc.telegram.Application', app_mock) - telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) + telegram, _, _ = get_telegram_testobject(mocker, default_conf) + telegram._app = app_mock + telegram._loop = asyncio.get_running_loop() + telegram._thread = MagicMock() telegram.cleanup() - assert telegram._updater.stop.call_count == 1 + await asyncio.sleep(0.1) + assert app_mock.stop.call_count == 1 + assert telegram._thread.join.call_count == 1 -def test_authorized_only(default_conf, mocker, caplog, update) -> None: +async def test_authorized_only(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) caplog.set_level(logging.DEBUG) default_conf['telegram']['enabled'] = False @@ -131,19 +202,19 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None: dummy = DummyCls(rpc, default_conf) patch_get_signal(bot) - dummy.dummy_handler(update=update, context=MagicMock()) + await dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is True assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog) assert not log_has('Rejected unauthorized message from: 0', caplog) assert not log_has('Exception occurred within Telegram module', caplog) -def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: +async def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: patch_exchange(mocker) caplog.set_level(logging.DEBUG) chat = Chat(0xdeadbeef, 0) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), datetime.utcnow(), chat) + message = Message(randint(1, 100), datetime.now(timezone.utc), chat) + update = Update(randint(1, 100), message=message) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -151,14 +222,14 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: dummy = DummyCls(rpc, default_conf) patch_get_signal(bot) - dummy.dummy_handler(update=update, context=MagicMock()) + await dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is False assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog) assert log_has('Rejected unauthorized message from: 3735928559', caplog) assert not log_has('Exception occurred within Telegram module', caplog) -def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None: +async def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) default_conf['telegram']['enabled'] = False @@ -168,17 +239,15 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None dummy = DummyCls(rpc, default_conf) patch_get_signal(bot) - dummy.dummy_exception(update=update, context=MagicMock()) + await dummy.dummy_exception(update=update, context=MagicMock()) assert dummy.state['called'] is False assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog) assert not log_has('Rejected unauthorized message from: 0', caplog) assert log_has('Exception occurred within Telegram module', caplog) -def test_telegram_status(default_conf, update, mocker) -> None: - update.message.chat.id = "123" +async def test_telegram_status(default_conf, update, mocker) -> None: default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" status_table = MagicMock() mocker.patch('freqtrade.rpc.telegram.Telegram._status_table', status_table) @@ -190,18 +259,22 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'pair': 'ETH/BTC', 'base_currency': 'ETH', 'quote_currency': 'BTC', - 'open_date': arrow.utcnow(), + 'open_date': dt_now(), 'close_date': None, 'open_rate': 1.099e-05, 'close_rate': None, 'current_rate': 1.098e-05, 'amount': 90.99181074, 'stake_amount': 90.99181074, + 'max_stake_amount': 90.99181074, 'buy_tag': None, 'enter_tag': None, 'close_profit_ratio': None, 'profit': -0.0059, 'profit_ratio': -0.0059, + 'profit_abs': -0.225, + 'realized_profit': 0.0, + 'total_profit_abs': -0.225, 'initial_stop_loss_abs': 1.098e-05, 'stop_loss_abs': 1.099e-05, 'exit_order_status': None, @@ -219,24 +292,22 @@ def test_telegram_status(default_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 context = MagicMock() # /status table context.args = ["table"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert status_table.call_count == 1 @pytest.mark.usefixtures("init_persistence") -def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: - update.message.chat.id = "123" +async def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" default_conf['position_adjustment_enable'] = True mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_order=MagicMock(return_value=None), get_rate=MagicMock(return_value=0.22), ) @@ -271,10 +342,11 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: trade.recalc_trade_from_orders() Trade.commit() - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 4 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Number of Entries.*2', msg) + assert re.search(r'Number of Exits.*0', msg) assert re.search(r'Average Entry Price', msg) assert re.search(r'Order filled', msg) assert re.search(r'Close Date:', msg) is None @@ -282,13 +354,10 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: @pytest.mark.usefixtures("init_persistence") -def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: - update.message.chat.id = "123" - default_conf['telegram']['enabled'] = False - default_conf['telegram']['chat_id'] = "123" +async def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: default_conf['position_adjustment_enable'] = True mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_order=MagicMock(return_value=None), get_rate=MagicMock(return_value=0.22), ) @@ -296,24 +365,23 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) create_mock_trades(fee) - trades = Trade.get_trades([Trade.is_open.is_(False)]) - trade = trades[0] + trade = Trade.get_trades([Trade.is_open.is_(False)]).first() context = MagicMock() context.args = [str(trade.id)] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert msg_mock.call_count == 1 msg = msg_mock.call_args_list[0][0][0] assert re.search(r'Close Date:', msg) assert re.search(r'Close Profit:', msg) -def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 3 mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, - _is_dry_limit_order_filled=MagicMock(return_value=True), + _dry_is_price_crossed=MagicMock(return_value=True), ) status_table = MagicMock() mocker.patch.multiple( @@ -327,13 +395,13 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED # Status is also enabled when stopped - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -341,7 +409,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() # Trigger status while we have a fulfilled order for the open trade - telegram._status(update=update, context=MagicMock()) + await telegram._status(update=update, context=MagicMock()) # close_rate should not be included in the message as the trade is not closed # and no line should be empty @@ -358,7 +426,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: context = MagicMock() context.args = ["2", "3"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) lines = msg_mock.call_args_list[0][0][0].split('\n') assert '' not in lines[:-1] @@ -373,7 +441,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() context = MagicMock() context.args = ["2"] - telegram._status(update=update, context=context) + await telegram._status(update=update, context=context) assert msg_mock.call_count == 2 @@ -385,9 +453,9 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: assert 'Trade ID:* `2` - continued' in msg2 -def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -400,13 +468,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED # Status table is also enabled when stopped - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -414,7 +482,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() - telegram._status_table(update=update, context=MagicMock()) + await telegram._status_table(update=update, context=MagicMock()) text = re.sub('', '', msg_mock.call_args_list[-1][0][0]) line = text.split("\n") @@ -426,13 +494,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg_mock.call_count == 1 -def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1 ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -448,11 +516,11 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # /daily 2 context = MagicMock() context.args = ["2"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert "Daily Profit over the last 2 days:" in msg_mock.call_args_list[0][0][0] assert 'Day ' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(datetime.now(timezone.utc).date()) in msg_mock.call_args_list[0][0][0] assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] @@ -462,11 +530,12 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert "Daily Profit over the last 7 days:" in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] - assert str((datetime.utcnow() - timedelta(days=5)).date()) in msg_mock.call_args_list[0][0][0] + assert str(datetime.now(timezone.utc).date()) in msg_mock.call_args_list[0][0][0] + assert str((datetime.now(timezone.utc) - timedelta(days=5)).date() + ) in msg_mock.call_args_list[0][0][0] assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] @@ -479,15 +548,15 @@ def test_daily_handle(default_conf_usdt, update, ticker, fee, mocker, time_machi # /daily 1 context = MagicMock() context.args = ["1"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert ' 6.83 USDT' in msg_mock.call_args_list[0][0][0] assert ' 7.51 USD' in msg_mock.call_args_list[0][0][0] assert '(2)' in msg_mock.call_args_list[0][0][0] -def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: +async def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker ) @@ -500,7 +569,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # /daily -2 context = MagicMock() context.args = ["-2"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -510,18 +579,18 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: # /daily today context = MagicMock() context.args = ["today"] - telegram._daily(update=update, context=context) + await telegram._daily(update=update, context=context) assert 'Daily Profit over the last 7 days:' in msg_mock.call_args_list[0][0][0] -def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1 ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -535,12 +604,12 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly 2 context = MagicMock() context.args = ["2"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert "Weekly Profit over the last 2 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] assert 'Monday ' in msg_mock.call_args_list[0][0][0] - today = datetime.utcnow().date() + today = datetime.now(timezone.utc).date() first_iso_day_of_current_week = today - timedelta(days=today.weekday()) assert str(first_iso_day_of_current_week) in msg_mock.call_args_list[0][0][0] assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] @@ -551,7 +620,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert "Weekly Profit over the last 8 weeks (starting from Monday):" \ in msg_mock.call_args_list[0][0][0] @@ -567,7 +636,7 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly -3 context = MagicMock() context.args = ["-3"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -577,21 +646,21 @@ def test_weekly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mach # /weekly this week context = MagicMock() context.args = ["this week"] - telegram._weekly(update=update, context=context) + await telegram._weekly(update=update, context=context) assert ( 'Weekly Profit over the last 8 weeks (starting from Monday):' in msg_mock.call_args_list[0][0][0] ) -def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: +async def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_machine) -> None: default_conf_usdt['max_open_trades'] = 1 mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1 ) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -605,11 +674,11 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly 2 context = MagicMock() context.args = ["2"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 2 months:' in msg_mock.call_args_list[0][0][0] assert 'Month ' in msg_mock.call_args_list[0][0][0] - today = datetime.utcnow().date() + today = datetime.now(timezone.utc).date() current_month = f"{today.year}-{today.month:02} " assert current_month in msg_mock.call_args_list[0][0][0] assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] @@ -620,7 +689,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # Reset msg_mock msg_mock.reset_mock() context.args = [] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 # Default to 6 months assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] @@ -637,7 +706,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly 12 context = MagicMock() context.args = ["12"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'Monthly Profit over the last 12 months:' in msg_mock.call_args_list[0][0][0] assert ' 2.74 USDT' in msg_mock.call_args_list[0][0][0] @@ -646,7 +715,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # The one-digit months should contain a zero, Eg: September 2021 = "2021-09" # Since we loaded the last 12 months, any month should appear - assert str('-09') in msg_mock.call_args_list[0][0][0] + assert '-09' in msg_mock.call_args_list[0][0][0] # Try invalid data msg_mock.reset_mock() @@ -654,7 +723,7 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly -3 context = MagicMock() context.args = ["-3"] - telegram._monthly(update=update, context=context) + await telegram._monthly(update=update, context=context) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] @@ -664,15 +733,16 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac # /monthly february context = MagicMock() context.args = ["february"] - telegram._monthly(update=update, context=context) - assert str('Monthly Profit over the last 6 months:') in msg_mock.call_args_list[0][0][0] + await telegram._monthly(update=update, context=context) + 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: +async 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( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_usdt, get_fee=fee, ) @@ -680,19 +750,19 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) patch_get_signal(freqtradebot) - telegram._profit(update=update, context=MagicMock()) + await telegram._profit(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() # Create some test data freqtradebot.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() context = MagicMock() # Test with invalid 2nd argument (should silently pass) context.args = ["aaa"] - telegram._profit(update=update, context=context) + await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] @@ -702,8 +772,9 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f msg_mock.reset_mock() # Update the ticker with a market going up - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_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) @@ -714,7 +785,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f Trade.commit() context.args = [3] - telegram._profit(update=update, context=context) + await telegram._profit(update=update, context=context) assert msg_mock.call_count == 1 assert '*ROI:* Closed trades' in msg_mock.call_args_list[-1][0][0] assert ('∙ `5.685 USDT (9.45%) (0.57 \N{GREEK CAPITAL LETTER SIGMA}%)`' @@ -732,17 +803,17 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f @pytest.mark.parametrize('is_short', [True, False]) -def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: +async def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._stats(update=update, context=MagicMock()) + await telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -750,68 +821,79 @@ def test_telegram_stats(default_conf, update, ticker, fee, mocker, is_short) -> # Create some test data create_mock_trades(fee, is_short=is_short) - telegram._stats(update=update, context=MagicMock()) + await telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Exit Reason' in msg_mock.call_args_list[-1][0][0] assert 'ROI' in msg_mock.call_args_list[-1][0][0] assert 'Avg. Duration' in msg_mock.call_args_list[-1][0][0] + # Duration is not only N/A + assert '0:19:00' in msg_mock.call_args_list[-1][0][0] + assert 'N/A' in msg_mock.call_args_list[-1][0][0] msg_mock.reset_mock() -def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: +async def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', - side_effect=lambda a, b: f"{a}/{b}") + mocker.patch(f'{EXMS}.get_balances', return_value=rpc_balance) + mocker.patch(f'{EXMS}.get_tickers', tickers) + mocker.patch(f'{EXMS}.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) + context = MagicMock() + context.args = ["full"] + await telegram._balance(update=update, context=context) result = msg_mock.call_args_list[0][0][0] - assert msg_mock.call_count == 1 + result_full = msg_mock.call_args_list[1][0][0] + assert msg_mock.call_count == 2 assert '*BTC:*' in result assert '*ETH:*' not in result assert '*USDT:*' not in result assert '*EUR:*' not in result - assert '*LTC:*' in result + assert '*LTC:*' not in result + + assert '*LTC:*' in result_full assert '*XRP:*' not in result assert 'Balance:' in result assert 'Est. BTC:' in result - assert 'BTC: 12' in result + assert 'BTC: 11' in result + assert 'BTC: 12' in result_full assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert 'BTC: 0.00000309' in result + assert '*Estimated Value*:' in result_full + assert '*Estimated Value (Bot managed assets only)*:' in result -def test_balance_handle_empty_response(default_conf, update, mocker) -> None: +async def test_balance_handle_empty_response(default_conf, update, mocker) -> None: default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) + mocker.patch(f'{EXMS}.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) freqtradebot.config['dry_run'] = False - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert 'Starting capital: `0 BTC' in result -def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) +async def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: + mocker.patch(f'{EXMS}.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert "*Warning:* Simulated balances in Dry Mode." in result assert "Starting capital: `1000 BTC`" in result -def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: +async def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: balances = [] for i in range(100): curr = choice(ascii_uppercase) + choice(ascii_uppercase) + choice(ascii_uppercase) @@ -820,18 +902,23 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'free': 1.0, 'used': 0.5, 'balance': i, + 'bot_owned': 0.5, 'est_stake': 1, + 'est_stake_bot': 1, 'stake': 'BTC', 'is_position': False, 'leverage': 1.0, 'position': 0.0, 'side': 'long', + 'is_bot_managed': True, }) mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ 'currencies': balances, 'total': 100.0, + 'total_bot': 100.0, 'symbol': 100.0, 'value': 1000.0, + 'value_bot': 1000.0, 'starting_capital': 1000, 'starting_capital_fiat': 1000, }) @@ -839,7 +926,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._balance(update=update, context=MagicMock()) + await telegram._balance(update=update, context=MagicMock()) assert msg_mock.call_count > 1 # Test if wrap happens around 4000 - # and each single currency-output is around 120 characters long so we need @@ -848,89 +935,89 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None assert len(msg_mock.call_args_list[0][0][0]) > (4096 - 120) -def test_start_handle(default_conf, update, mocker) -> None: +async def test_start_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED - telegram._start(update=update, context=MagicMock()) + await telegram._start(update=update, context=MagicMock()) assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 -def test_start_handle_already_running(default_conf, update, mocker) -> None: +async def test_start_handle_already_running(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._start(update=update, context=MagicMock()) + await telegram._start(update=update, context=MagicMock()) assert freqtradebot.state == State.RUNNING assert msg_mock.call_count == 1 assert 'already running' in msg_mock.call_args_list[0][0][0] -def test_stop_handle(default_conf, update, mocker) -> None: +async def test_stop_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._stop(update=update, context=MagicMock()) + await telegram._stop(update=update, context=MagicMock()) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'stopping trader' in msg_mock.call_args_list[0][0][0] -def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: +async def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED - telegram._stop(update=update, context=MagicMock()) + await telegram._stop(update=update, context=MagicMock()) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 assert 'already stopped' in msg_mock.call_args_list[0][0][0] -def test_stopbuy_handle(default_conf, update, mocker) -> None: +async def test_stopbuy_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) assert freqtradebot.config['max_open_trades'] != 0 - telegram._stopentry(update=update, context=MagicMock()) + await telegram._stopentry(update=update, context=MagicMock()) assert freqtradebot.config['max_open_trades'] == 0 assert msg_mock.call_count == 1 assert 'No more entries will occur from now. Run /reload_config to reset.' \ in msg_mock.call_args_list[0][0][0] -def test_reload_config_handle(default_conf, update, mocker) -> None: +async def test_reload_config_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._reload_config(update=update, context=MagicMock()) + await telegram._reload_config(update=update, context=MagicMock()) assert freqtradebot.state == State.RELOAD_CONFIG assert msg_mock.call_count == 1 assert 'Reloading config' in msg_mock.call_args_list[0][0][0] -def test_telegram_forceexit_handle(default_conf, update, ticker, fee, - ticker_sell_up, mocker) -> None: +async def test_telegram_forceexit_handle(default_conf, update, ticker, fee, + ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) patch_whitelist(mocker, default_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, - _is_dry_limit_order_filled=MagicMock(return_value=True), + _dry_is_price_crossed=MagicMock(return_value=True), ) freqtradebot = FreqtradeBot(default_conf) @@ -941,16 +1028,16 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, # Create some test data freqtradebot.enter_positions() - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade # Increase the price and sell it - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) + mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up) # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-2][0][0] @@ -986,8 +1073,8 @@ def test_telegram_forceexit_handle(default_conf, update, ticker, fee, } == last_msg -def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, - ticker_sell_down, mocker) -> None: +async def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, + ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -996,10 +1083,10 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, patch_whitelist(mocker, default_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, - _is_dry_limit_order_filled=MagicMock(return_value=True), + _dry_is_price_crossed=MagicMock(return_value=True), ) freqtradebot = FreqtradeBot(default_conf) @@ -1012,17 +1099,17 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, # Decrease the price and sell it mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker_sell_down ) - trade = Trade.query.first() + trade = Trade.session.scalars(select(Trade)).first() assert trade # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 4 @@ -1059,7 +1146,7 @@ def test_telegram_force_exit_down_handle(default_conf, update, ticker, fee, } == last_msg -def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None: patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -1067,10 +1154,10 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_whitelist(mocker, default_conf) mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, - _is_dry_limit_order_filled=MagicMock(return_value=True), + _dry_is_price_crossed=MagicMock(return_value=True), ) default_conf['max_open_trades'] = 4 freqtradebot = FreqtradeBot(default_conf) @@ -1085,7 +1172,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None # /forceexit all context = MagicMock() context.args = ["all"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # Called for each trade 2 times assert msg_mock.call_count == 8 @@ -1122,7 +1209,7 @@ def test_forceexit_all_handle(default_conf, update, ticker, fee, mocker) -> None } == msg -def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: +async def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -1134,7 +1221,7 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: # /forceexit 1 context = MagicMock() context.args = ["1"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] @@ -1144,18 +1231,18 @@ def test_forceexit_handle_invalid(default_conf, update, mocker) -> None: # /forceexit 123456 context = MagicMock() context.args = ["123456"] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) assert msg_mock.call_count == 1 assert 'invalid argument' in msg_mock.call_args_list[0][0][0] -def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: +async def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 4 mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, - _is_dry_limit_order_filled=MagicMock(return_value=True), + _dry_is_price_crossed=MagicMock(return_value=True), ) femock = mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_exit') telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1165,7 +1252,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: # /forceexit context = MagicMock() context.args = [] - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # No pair assert msg_mock.call_args_list[0][1]['msg'] == 'No open trade found.' @@ -1174,7 +1261,7 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() # /forceexit - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) keyboard = msg_mock.call_args_list[0][1]['keyboard'] # 4 pairs + cancel assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 @@ -1182,9 +1269,9 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: assert keyboard[1][0].callback_data == 'force_exit__2 ' update = MagicMock() - update.callback_query = MagicMock() + update.callback_query = AsyncMock() update.callback_query.data = keyboard[1][0].callback_data - telegram._force_exit_inline(update, None) + await telegram._force_exit_inline(update, None) assert update.callback_query.answer.call_count == 1 assert update.callback_query.edit_message_text.call_count == 1 assert femock.call_count == 1 @@ -1192,21 +1279,21 @@ def test_force_exit_no_pair(default_conf, update, ticker, fee, mocker) -> None: # Retry exiting - but cancel instead update.callback_query.reset_mock() - telegram._force_exit(update=update, context=context) + await telegram._force_exit(update=update, context=context) # Use cancel button update.callback_query.data = keyboard[-1][0].callback_data - telegram._force_exit_inline(update, None) + await telegram._force_exit_inline(update, None) query = update.callback_query assert query.answer.call_count == 1 assert query.edit_message_text.call_count == 1 assert query.edit_message_text.call_args_list[-1][1]['text'] == "Force exit canceled." -def test_force_enter_handle(default_conf, update, mocker) -> None: +async def test_force_enter_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock) telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) @@ -1214,7 +1301,7 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # /forcelong ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1223,11 +1310,11 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: # Reset and retry with specified price fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock) # /forcelong ETH/BTC 0.055 context = MagicMock() context.args = ["ETH/BTC", "0.055"] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1235,24 +1322,23 @@ def test_force_enter_handle(default_conf, update, mocker) -> None: assert fbuy_mock.call_args_list[0][0][1] == 0.055 -def test_force_enter_handle_exception(default_conf, update, mocker) -> None: +async def test_force_enter_handle_exception(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - update.message.text = '/forcebuy ETH/Nonepair' - telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) assert msg_mock.call_count == 1 assert msg_mock.call_args_list[0][0][0] == 'Force_entry not enabled.' -def test_force_enter_no_pair(default_conf, update, mocker) -> None: +async def test_force_enter_no_pair(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_force_entry', fbuy_mock) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1260,7 +1346,7 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) + await telegram._force_enter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 0 assert msg_mock.call_count == 1 @@ -1270,16 +1356,16 @@ def test_force_enter_no_pair(default_conf, update, mocker) -> None: # One additional button - cancel assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 update = MagicMock() - update.callback_query = MagicMock() + update.callback_query = AsyncMock() update.callback_query.data = 'XRP/USDT_||_long' - telegram._force_enter_inline(update, None) + await telegram._force_enter_inline(update, None) assert fbuy_mock.call_count == 1 -def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: +async def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1288,16 +1374,16 @@ def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, moc # Create some test data create_mock_trades_usdt(fee) - telegram._performance(update=update, context=MagicMock()) + await telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'XRP/USDT\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] -def test_telegram_entry_tag_performance_handle( +async def test_telegram_entry_tag_performance_handle( default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1307,28 +1393,28 @@ def test_telegram_entry_tag_performance_handle( create_mock_trades_usdt(fee) context = MagicMock() - telegram._enter_tag_performance(update=update, context=context) + await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0] assert 'TEST1\t3.987 USDT (5.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] - telegram._enter_tag_performance(update=update, context=context) + await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_enter_tag_performance', side_effect=RPCException('Error')) - telegram._enter_tag_performance(update=update, context=MagicMock()) + await telegram._enter_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, ticker, fee, - mocker) -> None: +async def test_telegram_exit_reason_performance_handle( + default_conf_usdt, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1338,28 +1424,28 @@ def test_telegram_exit_reason_performance_handle(default_conf_usdt, update, tick create_mock_trades_usdt(fee) context = MagicMock() - telegram._exit_reason_performance(update=update, context=context) + await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0] assert 'roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] - telegram._exit_reason_performance(update=update, context=context) + await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_exit_reason_performance', side_effect=RPCException('Error')) - telegram._exit_reason_performance(update=update, context=MagicMock()) + await telegram._exit_reason_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, - mocker) -> None: +async def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, fee, + mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1370,28 +1456,28 @@ def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ticker, create_mock_trades_usdt(fee) context = MagicMock() - telegram._mix_tag_performance(update=update, context=context) + await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] assert ('TEST3 roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0]) context.args = ['XRP/USDT'] - telegram._mix_tag_performance(update=update, context=context) + await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 2 msg_mock.reset_mock() mocker.patch('freqtrade.rpc.rpc.RPC._rpc_mix_tag_performance', side_effect=RPCException('Error')) - telegram._mix_tag_performance(update=update, context=MagicMock()) + await telegram._mix_tag_performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Error" in msg_mock.call_args_list[0][0][0] -def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) @@ -1399,7 +1485,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: patch_get_signal(freqtradebot) freqtradebot.state = State.STOPPED - telegram._count(update=update, context=MagicMock()) + await telegram._count(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -1408,7 +1494,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: # Create some test data freqtradebot.enter_positions() msg_mock.reset_mock() - telegram._count(update=update, context=MagicMock()) + await telegram._count(update=update, context=MagicMock()) msg = ('
  current    max    total stake\n---------  -----  -------------\n'
            '        1      {}          {}
').format( @@ -1418,24 +1504,24 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg in msg_mock.call_args_list[0][0][0] -def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: +async def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( - 'freqtrade.exchange.Exchange', + EXMS, fetch_ticker=ticker, get_fee=fee, ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - telegram._locks(update=update, context=MagicMock()) + await telegram._locks(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No active locks.' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') - PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') + PairLocks.lock_pair('ETH/BTC', dt_now() + timedelta(minutes=4), 'randreason') + PairLocks.lock_pair('XRP/BTC', dt_now() + timedelta(minutes=20), 'deadbeef') - telegram._locks(update=update, context=MagicMock()) + await telegram._locks(update=update, context=MagicMock()) assert 'Pair' in msg_mock.call_args_list[0][0][0] assert 'Until' in msg_mock.call_args_list[0][0][0] @@ -1448,7 +1534,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None context = MagicMock() context.args = ['XRP/BTC'] msg_mock.reset_mock() - telegram._delete_locks(update=update, context=context) + await telegram._delete_locks(update=update, context=context) assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] assert 'randreason' in msg_mock.call_args_list[0][0][0] @@ -1456,11 +1542,11 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None assert 'deadbeef' not in msg_mock.call_args_list[0][0][0] -def test_whitelist_static(default_conf, update, mocker) -> None: +async def test_whitelist_static(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._whitelist(update=update, context=MagicMock()) + await telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1468,33 +1554,33 @@ def test_whitelist_static(default_conf, update, mocker) -> None: context = MagicMock() context.args = ['sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, NEO/BTC, XRP/BTC`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH, LTC, XRP, NEO`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly', 'sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['StaticPairList']` with 4 pairs\n" "`ETH, LTC, NEO, XRP`" in msg_mock.call_args_list[0][0][0]) -def test_whitelist_dynamic(default_conf, update, mocker) -> None: - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) +async def test_whitelist_dynamic(default_conf, update, mocker) -> None: + mocker.patch(f'{EXMS}.exchange_has', return_value=True) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._whitelist(update=update, context=MagicMock()) + await telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1502,30 +1588,30 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: context = MagicMock() context.args = ['sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH/BTC, LTC/BTC, NEO/BTC, XRP/BTC`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH, LTC, XRP, NEO`" in msg_mock.call_args_list[0][0][0]) context = MagicMock() context.args = ['baseonly', 'sorted'] msg_mock.reset_mock() - telegram._whitelist(update=update, context=context) + await telegram._whitelist(update=update, context=context) assert ("Using whitelist `['VolumePairList']` with 4 pairs\n" "`ETH, LTC, NEO, XRP`" in msg_mock.call_args_list[0][0][0]) -def test_blacklist_static(default_conf, update, mocker) -> None: +async def test_blacklist_static(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._blacklist(update=update, context=MagicMock()) + await telegram._blacklist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert ("Blacklist contains 2 pairs\n`DOGE/BTC, HOT/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1535,7 +1621,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: # /blacklist ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._blacklist(update=update, context=context) + await telegram._blacklist(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 3 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC`" in msg_mock.call_args_list[0][0][0]) @@ -1544,7 +1630,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context = MagicMock() context.args = ["XRP/.*"] - telegram._blacklist(update=update, context=context) + await telegram._blacklist(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 4 pairs\n`DOGE/BTC, HOT/BTC, ETH/BTC, XRP/.*`" @@ -1553,13 +1639,13 @@ def test_blacklist_static(default_conf, update, mocker) -> None: msg_mock.reset_mock() context.args = ["DOGE/BTC"] - telegram._blacklist_delete(update=update, context=context) + await telegram._blacklist_delete(update=update, context=context) assert msg_mock.call_count == 1 assert ("Blacklist contains 3 pairs\n`HOT/BTC, ETH/BTC, XRP/.*`" in msg_mock.call_args_list[0][0][0]) -def test_telegram_logs(default_conf, update, mocker) -> None: +async def test_telegram_logs(default_conf, update, mocker) -> None: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), @@ -1570,13 +1656,13 @@ def test_telegram_logs(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 assert "freqtrade\\.rpc\\.telegram" in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() context.args = ["1"] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 msg_mock.reset_mock() @@ -1584,22 +1670,22 @@ def test_telegram_logs(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.MAX_MESSAGE_LENGTH', 200) context = MagicMock() context.args = [] - telegram._logs(update=update, context=context) + await telegram._logs(update=update, context=context) # Called at least 2 times. Exact times will change with unrelated changes to setup messages # Therefore we don't test for this explicitly. assert msg_mock.call_count >= 2 -def test_edge_disabled(default_conf, update, mocker) -> None: +async def test_edge_disabled(default_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) - telegram._edge(update=update, context=MagicMock()) + await telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert "Edge is not enabled." in msg_mock.call_args_list[0][0][0] -def test_edge_enabled(edge_conf, update, mocker) -> None: +async def test_edge_enabled(edge_conf, update, mocker) -> None: mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), @@ -1608,7 +1694,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, edge_conf) - telegram._edge(update=update, context=MagicMock()) + await telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'Edge only validated following pairs:\n
' in msg_mock.call_args_list[0][0][0]
     assert 'Pair      Winrate    Expectancy    Stoploss' in msg_mock.call_args_list[0][0][0]
@@ -1617,7 +1703,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 
     mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock(
         return_value={}))
-    telegram._edge(update=update, context=MagicMock())
+    await telegram._edge(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert 'Edge only validated following pairs:' in msg_mock.call_args_list[0][0][0]
     assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
@@ -1626,20 +1712,20 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
 @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):
+async def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
     context = MagicMock()
     context.args = []
 
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
 
     context.args = ['hello']
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
@@ -1648,7 +1734,7 @@ def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_patt
 
     context = MagicMock()
     context.args = [5]
-    telegram._trades(update=update, context=context)
+    await telegram._trades(update=update, context=context)
     msg_mock.call_count == 1
     assert "2 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
@@ -1658,13 +1744,13 @@ def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_patt
 
 
 @pytest.mark.parametrize('is_short', [True, False])
-def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
+async def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     context = MagicMock()
     context.args = []
 
-    telegram._delete_trade(update=update, context=context)
+    await telegram._delete_trade(update=update, context=context)
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
@@ -1672,44 +1758,96 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
 
     context = MagicMock()
     context.args = [1]
-    telegram._delete_trade(update=update, context=context)
+    await telegram._delete_trade(update=update, context=context)
     msg_mock.call_count == 1
     assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
     assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
 
 
-def test_help_handle(default_conf, update, mocker) -> None:
+@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):
+
+    mocker.patch.multiple(
+        EXMS,
+        fetch_ticker=ticker,
+    )
+    telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
+    context = MagicMock()
+    context.args = []
+
+    await telegram._cancel_open_order(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 = MagicMock()
+    context.args = [5]
+    await telegram._cancel_open_order(update=update, context=context)
+    assert "No open order for trade_id" in msg_mock.call_args_list[0][0][0]
+
+    msg_mock.reset_mock()
+
+    trade = Trade.get_trades([Trade.id == 6]).first()
+    mocker.patch(f'{EXMS}.fetch_order', return_value=trade.orders[-1].to_ccxt_object())
+    context = MagicMock()
+    context.args = [6]
+    await telegram._cancel_open_order(update=update, context=context)
+    assert msg_mock.call_count == 1
+    assert "Open order canceled." in msg_mock.call_args_list[0][0][0]
+
+
+async def test_help_handle(default_conf, update, mocker) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._help(update=update, context=MagicMock())
+    await telegram._help(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
 
 
-def test_version_handle(default_conf, update, mocker) -> None:
+async def test_version_handle(default_conf, update, mocker) -> None:
 
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._version(update=update, context=MagicMock())
+    await telegram._version(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
-    assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
+    assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
     freqtradebot.strategy.version = lambda: '1.1.1'
 
-    telegram._version(update=update, context=MagicMock())
+    await telegram._version(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
-    assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
+    assert f'*Version:* `{__version__}`' in msg_mock.call_args_list[0][0][0]
     assert '*Strategy version: * `1.1.1`' in msg_mock.call_args_list[0][0][0]
 
 
-def test_show_config_handle(default_conf, update, mocker) -> None:
+async def test_show_config_handle(default_conf, update, mocker) -> None:
 
     default_conf['runmode'] = RunMode.DRY_RUN
 
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
-    telegram._show_config(update=update, context=MagicMock())
+    await telegram._show_config(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
     assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
@@ -1718,7 +1856,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
 
     msg_mock.reset_mock()
     freqtradebot.config['trailing_stop'] = True
-    telegram._show_config(update=update, context=MagicMock())
+    await telegram._show_config(update=update, context=MagicMock())
     assert msg_mock.call_count == 1
     assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
     assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
@@ -1760,7 +1898,7 @@ def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type,
         'current_rate': 1.099e-05,
         'amount': 1333.3333333333335,
         'analyzed_candle': {'open': 1.1, 'high': 2.2, 'low': 1.0, 'close': 1.5},
-        'open_date': arrow.utcnow().shift(hours=-1)
+        'open_date': dt_now() + timedelta(hours=-1)
     }
     telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
 
@@ -1821,7 +1959,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) ->
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     time_machine.move_to("2021-09-01 05:00:00 +00:00")
-    lock = PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=6).datetime, 'randreason')
+    lock = PairLocks.lock_pair('ETH/BTC', dt_now() + timedelta(minutes=6), 'randreason')
     msg = {
         'type': RPCMessageType.PROTECTION_TRIGGER,
     }
@@ -1836,7 +1974,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) ->
     msg = {
         'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
     }
-    lock = PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=100).datetime, 'randreason')
+    lock = PairLocks.lock_pair('*', dt_now() + timedelta(minutes=100), 'randreason')
     msg.update(lock.to_json())
     telegram.send_msg(msg)
     assert (msg_mock.call_args[0][0] == "*Protection* triggered due to randreason. "
@@ -1867,7 +2005,7 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
         'fiat_currency': 'USD',
         'open_rate': 1.099e-05,
         'amount': 1333.3333333333335,
-        'open_date': arrow.utcnow().shift(hours=-1)
+        'open_date': dt_now() - timedelta(hours=1)
     })
     leverage_text = f'*Leverage:* `{leverage}`\n' if leverage != 1.0 else ''
     assert msg_mock.call_args[0][0] == (
@@ -1894,7 +2032,7 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en
         'fiat_currency': 'USD',
         'open_rate': 1.099e-05,
         'amount': 1333.3333333333335,
-        'open_date': arrow.utcnow().shift(hours=-1)
+        'open_date': dt_now() - timedelta(hours=1)
     })
 
     assert msg_mock.call_args[0][0] == (
@@ -1933,8 +2071,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
             'fiat_currency': 'USD',
             'enter_tag': 'buy_signal1',
             'exit_reason': ExitType.STOP_LOSS.value,
-            'open_date': arrow.utcnow().shift(hours=-1),
-            'close_date': arrow.utcnow(),
+            'open_date': dt_now() - timedelta(hours=1),
+            'close_date': dt_now(),
         })
         assert msg_mock.call_args[0][0] == (
             '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
@@ -1969,13 +2107,13 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
             'fiat_currency': 'USD',
             'enter_tag': 'buy_signal1',
             'exit_reason': ExitType.STOP_LOSS.value,
-            'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
-            'close_date': arrow.utcnow(),
+            'open_date': dt_now() - timedelta(days=1, hours=2, minutes=30),
+            'close_date': dt_now(),
             'stake_amount': 0.01,
             'sub_trade': True,
         })
         assert msg_mock.call_args[0][0] == (
-            '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
+            '\N{WARNING SIGN} *Binance (dry):* Partially exiting KEY/ETH (#1)\n'
             '*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n'
             '*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n'
             '*Enter Tag:* `buy_signal1`\n'
@@ -2006,8 +2144,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
             'stake_currency': 'ETH',
             'enter_tag': 'buy_signal1',
             'exit_reason': ExitType.STOP_LOSS.value,
-            'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
-            'close_date': arrow.utcnow(),
+            'open_date': dt_now() - timedelta(days=1, hours=2, minutes=30),
+            'close_date': dt_now(),
         })
         assert msg_mock.call_args[0][0] == (
             '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n'
@@ -2025,7 +2163,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
         telegram._rpc._fiat_converter.convert_amount = old_convamount
 
 
-def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
+async def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
@@ -2088,8 +2226,8 @@ def test_send_msg_sell_fill_notification(default_conf, mocker, direction,
             'stake_currency': 'ETH',
             'enter_tag': enter_signal,
             'exit_reason': ExitType.STOP_LOSS.value,
-            'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30),
-            'close_date': arrow.utcnow(),
+            'open_date': dt_now() - timedelta(days=1, hours=2, minutes=30),
+            'close_date': dt_now(),
         })
 
         leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
@@ -2117,7 +2255,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
     assert msg_mock.call_args[0][0] == '*Status:* `running`'
 
 
-def test_warning_notification(default_conf, mocker) -> None:
+async def test_warning_notification(default_conf, mocker) -> None:
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
     telegram.send_msg({
         'type': RPCMessageType.WARNING,
@@ -2179,7 +2317,7 @@ def test_send_msg_buy_notification_no_fiat(
         'fiat_currency': None,
         'current_rate': 1.099e-05,
         'amount': 1333.3333333333335,
-        'open_date': arrow.utcnow().shift(hours=-1)
+        'open_date': dt_now() - timedelta(hours=1)
     })
 
     leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
@@ -2201,8 +2339,9 @@ def test_send_msg_buy_notification_no_fiat(
     ('Short', 'short_signal_01', 2.0),
 ])
 def test_send_msg_sell_notification_no_fiat(
-        default_conf, mocker, direction, enter_signal, leverage) -> None:
+        default_conf, mocker, direction, enter_signal, leverage, time_machine) -> None:
     del default_conf['fiat_display_currency']
+    time_machine.move_to('2022-05-02 00:00:00 +00:00', tick=False)
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
 
     telegram.send_msg({
@@ -2224,8 +2363,8 @@ def test_send_msg_sell_notification_no_fiat(
         'fiat_currency': 'USD',
         'enter_tag': enter_signal,
         'exit_reason': ExitType.STOP_LOSS.value,
-        'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3),
-        'close_date': arrow.utcnow(),
+        'open_date': dt_now() - timedelta(hours=2, minutes=35, seconds=3),
+        'close_date': dt_now(),
     })
 
     leverage_text = f'*Leverage:* `{leverage}`\n' if leverage and leverage != 1.0 else ''
@@ -2261,60 +2400,62 @@ def test__sell_emoji(default_conf, mocker, msg, expected):
     assert telegram._get_sell_emoji(msg) == expected
 
 
-def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
+async def test_telegram__send_msg(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
+    bot.send_message = AsyncMock()
+    bot.edit_message_text = AsyncMock()
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
-    telegram._updater = MagicMock()
-    telegram._updater.bot = bot
+    telegram._app = MagicMock()
+    telegram._app.bot = bot
 
-    telegram._config['telegram']['enabled'] = True
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
     assert len(bot.method_calls) == 1
 
     # Test update
     query = MagicMock()
-    telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
-    edit_message_text = telegram._updater.bot.edit_message_text
+    await telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True)
+    edit_message_text = telegram._app.bot.edit_message_text
     assert edit_message_text.call_count == 1
     assert "Updated: " in edit_message_text.call_args_list[0][1]['text']
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("not modified"))
-    telegram._send_msg('test', callback_path="DeadBeef", query=query)
-    assert telegram._updater.bot.edit_message_text.call_count == 1
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("not modified"))
+    await telegram._send_msg('test', callback_path="DeadBeef", query=query)
+    assert telegram._app.bot.edit_message_text.call_count == 1
     assert not log_has_re(r"TelegramError: .*", caplog)
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest(""))
-    telegram._send_msg('test2', callback_path="DeadBeef", query=query)
-    assert telegram._updater.bot.edit_message_text.call_count == 1
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest(""))
+    await telegram._send_msg('test2', callback_path="DeadBeef", query=query)
+    assert telegram._app.bot.edit_message_text.call_count == 1
     assert log_has_re(r"TelegramError: .*", caplog)
 
-    telegram._updater.bot.edit_message_text = MagicMock(side_effect=TelegramError("DeadBEEF"))
-    telegram._send_msg('test3', callback_path="DeadBeef", query=query)
+    telegram._app.bot.edit_message_text = AsyncMock(side_effect=TelegramError("DeadBEEF"))
+    await telegram._send_msg('test3', callback_path="DeadBeef", query=query)
 
     assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog)
 
 
-def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
+async def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
     bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
     telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
-    telegram._updater = MagicMock()
-    telegram._updater.bot = bot
+    telegram._app = MagicMock()
+    telegram._app.bot = bot
 
     telegram._config['telegram']['enabled'] = True
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
 
     # Bot should've tried to send it twice
     assert len(bot.method_calls) == 2
     assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog)
 
 
-def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
+@pytest.mark.filterwarnings("ignore:.*ChatPermissions")
+async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
     mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
     bot = MagicMock()
-    bot.send_message = MagicMock()
+    bot.send_message = AsyncMock()
     freqtradebot = get_patched_freqtradebot(mocker, default_conf)
     rpc = RPC(freqtradebot)
 
@@ -2330,14 +2471,14 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
 
     def init_telegram(freqtradebot):
         telegram = Telegram(rpc, default_conf)
-        telegram._updater = MagicMock()
-        telegram._updater.bot = bot
+        telegram._app = MagicMock()
+        telegram._app.bot = bot
         return telegram
 
     # no keyboard in config -> default keyboard
     freqtradebot.config['telegram']['enabled'] = True
     telegram = init_telegram(freqtradebot)
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
     used_keyboard = bot.send_message.call_args[1]['reply_markup']
     assert used_keyboard == default_keyboard
 
@@ -2354,9 +2495,22 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None:
     freqtradebot.config['telegram']['enabled'] = True
     freqtradebot.config['telegram']['keyboard'] = custom_keys_list
     telegram = init_telegram(freqtradebot)
-    telegram._send_msg('test')
+    await telegram._send_msg('test')
     used_keyboard = bot.send_message.call_args[1]['reply_markup']
     assert used_keyboard == custom_keyboard
     assert log_has("using custom keyboard from config.json: "
                    "[['/daily', '/stats', '/balance', '/profit', '/profit 5'], ['/count', "
                    "'/start', '/reload_config', '/help']]", caplog)
+
+
+async def test_change_market_direction(default_conf, mocker, update) -> None:
+    telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
+    assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.NONE
+    context = MagicMock()
+    context.args = ["long"]
+    await telegram._changemarketdir(update, context)
+    assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG
+    context = MagicMock()
+    context.args = ["invalid"]
+    await telegram._changemarketdir(update, context)
+    assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG
diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py
index a8fd0c34b..d0a0f5b1e 100644
--- a/tests/rpc/test_rpc_webhook.py
+++ b/tests/rpc/test_rpc_webhook.py
@@ -17,6 +17,10 @@ def get_webhook_dict() -> dict:
         "enabled": True,
         "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
         "webhookentry": {
+            # Intentionally broken, as "entry" should have priority.
+            "value1": "Buying {pair55555}",
+        },
+        "entry": {
             "value1": "Buying {pair}",
             "value2": "limit {limit:8f}",
             "value3": "{stake_amount:8f} {stake_currency}",
@@ -89,15 +93,15 @@ def test_send_msg_webhook(default_conf, mocker):
     webhook.send_msg(msg=msg)
     assert msg_mock.call_count == 1
     assert (msg_mock.call_args[0][0]["value1"] ==
-            default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
+            default_conf["webhook"]["entry"]["value1"].format(**msg))
     assert (msg_mock.call_args[0][0]["value2"] ==
-            default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
+            default_conf["webhook"]["entry"]["value2"].format(**msg))
     assert (msg_mock.call_args[0][0]["value3"] ==
-            default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
+            default_conf["webhook"]["entry"]["value3"].format(**msg))
     assert (msg_mock.call_args[0][0]["value4"] ==
-            default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
+            default_conf["webhook"]["entry"]["value4"].format(**msg))
     assert (msg_mock.call_args[0][0]["value5"] ==
-            default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
+            default_conf["webhook"]["entry"]["value5"].format(**msg))
     # Test short
     msg_mock.reset_mock()
 
@@ -116,15 +120,15 @@ def test_send_msg_webhook(default_conf, mocker):
     webhook.send_msg(msg=msg)
     assert msg_mock.call_count == 1
     assert (msg_mock.call_args[0][0]["value1"] ==
-            default_conf["webhook"]["webhookentry"]["value1"].format(**msg))
+            default_conf["webhook"]["entry"]["value1"].format(**msg))
     assert (msg_mock.call_args[0][0]["value2"] ==
-            default_conf["webhook"]["webhookentry"]["value2"].format(**msg))
+            default_conf["webhook"]["entry"]["value2"].format(**msg))
     assert (msg_mock.call_args[0][0]["value3"] ==
-            default_conf["webhook"]["webhookentry"]["value3"].format(**msg))
+            default_conf["webhook"]["entry"]["value3"].format(**msg))
     assert (msg_mock.call_args[0][0]["value4"] ==
-            default_conf["webhook"]["webhookentry"]["value4"].format(**msg))
+            default_conf["webhook"]["entry"]["value4"].format(**msg))
     assert (msg_mock.call_args[0][0]["value5"] ==
-            default_conf["webhook"]["webhookentry"]["value5"].format(**msg))
+            default_conf["webhook"]["entry"]["value5"].format(**msg))
     # Test buy cancel
     msg_mock.reset_mock()
 
@@ -328,6 +332,7 @@ def test_send_msg_webhook(default_conf, mocker):
 
 def test_exception_send_msg(default_conf, mocker, caplog):
     default_conf["webhook"] = get_webhook_dict()
+    del default_conf["webhook"]["entry"]
     del default_conf["webhook"]["webhookentry"]
 
     webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
@@ -356,6 +361,14 @@ def test_exception_send_msg(default_conf, mocker, caplog):
             }
         webhook.send_msg(msg)
 
+    # Test no failure for not implemented but known messagetypes
+    for e in RPCMessageType:
+        msg = {
+            'type': e,
+            'status': 'whatever'
+            }
+        webhook.send_msg(msg)
+
 
 def test__send_msg(default_conf, mocker, caplog):
     default_conf["webhook"] = get_webhook_dict()
diff --git a/tests/strategy/strats/broken_strats/broken_futures_strategies.py b/tests/strategy/strats/broken_strats/broken_futures_strategies.py
index 7e6955d37..bb7ce2b32 100644
--- a/tests/strategy/strats/broken_strats/broken_futures_strategies.py
+++ b/tests/strategy/strats/broken_strats/broken_futures_strategies.py
@@ -7,6 +7,7 @@ from datetime import datetime
 
 from pandas import DataFrame
 
+from freqtrade.persistence.trade_model import Order
 from freqtrade.strategy.interface import IStrategy
 
 
@@ -35,7 +36,7 @@ class TestStrategyImplementBuyTimeout(TestStrategyNoImplementSell):
     def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
         return super().populate_exit_trend(dataframe, metadata)
 
-    def check_buy_timeout(self, pair: str, trade, order: dict,
+    def check_buy_timeout(self, pair: str, trade, order: Order,
                           current_time: datetime, **kwargs) -> bool:
         return False
 
@@ -44,6 +45,6 @@ class TestStrategyImplementSellTimeout(TestStrategyNoImplementSell):
     def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
         return super().populate_exit_trend(dataframe, metadata)
 
-    def check_sell_timeout(self, pair: str, trade, order: dict,
+    def check_sell_timeout(self, pair: str, trade, order: Order,
                            current_time: datetime, **kwargs) -> bool:
         return False
diff --git a/tests/strategy/strats/freqai_rl_test_strat.py b/tests/strategy/strats/freqai_rl_test_strat.py
index 6fa926fc9..2bf4aaa30 100644
--- a/tests/strategy/strats/freqai_rl_test_strat.py
+++ b/tests/strategy/strats/freqai_rl_test_strat.py
@@ -1,5 +1,6 @@
 import logging
 from functools import reduce
+from typing import Dict
 
 import talib.abstract as ta
 from pandas import DataFrame
@@ -24,20 +25,21 @@ class freqai_rl_test_strat(IStrategy):
     startup_candle_count: int = 300
     can_short = False
 
-    def feature_engineering_expand_all(self, dataframe, period, **kwargs):
+    def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
+                                       metadata: Dict, **kwargs):
 
         dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
 
         return dataframe
 
-    def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs):
+    def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-pct-change"] = dataframe["close"].pct_change()
         dataframe["%-raw_volume"] = dataframe["volume"]
 
         return dataframe
 
-    def feature_engineering_standard(self, dataframe, **kwargs):
+    def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
         dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
@@ -49,7 +51,7 @@ class freqai_rl_test_strat(IStrategy):
 
         return dataframe
 
-    def set_freqai_targets(self, dataframe, **kwargs):
+    def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["&-action"] = 0
 
diff --git a/tests/strategy/strats/freqai_test_classifier.py b/tests/strategy/strats/freqai_test_classifier.py
index 02427ab59..a68a87b2a 100644
--- a/tests/strategy/strats/freqai_test_classifier.py
+++ b/tests/strategy/strats/freqai_test_classifier.py
@@ -1,5 +1,6 @@
 import logging
 from functools import reduce
+from typing import Dict
 
 import numpy as np
 import talib.abstract as ta
@@ -56,7 +57,8 @@ class freqai_test_classifier(IStrategy):
                 informative_pairs.append((pair, tf))
         return informative_pairs
 
-    def feature_engineering_expand_all(self, dataframe, period, **kwargs):
+    def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
+                                       metadata: Dict, **kwargs):
 
         dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
         dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
@@ -64,7 +66,7 @@ class freqai_test_classifier(IStrategy):
 
         return dataframe
 
-    def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs):
+    def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-pct-change"] = dataframe["close"].pct_change()
         dataframe["%-raw_volume"] = dataframe["volume"]
@@ -72,15 +74,15 @@ class freqai_test_classifier(IStrategy):
 
         return dataframe
 
-    def feature_engineering_standard(self, dataframe, **kwargs):
+    def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
         dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
 
         return dataframe
 
-    def set_freqai_targets(self, dataframe, **kwargs):
-
+    def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
+        self.freqai.class_names = ["down", "up"]
         dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) >
                                               dataframe["close"], 'up', 'down')
 
diff --git a/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py b/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py
index 65f2e4540..b2ddc21e3 100644
--- a/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py
+++ b/tests/strategy/strats/freqai_test_multimodel_classifier_strat.py
@@ -1,5 +1,6 @@
 import logging
 from functools import reduce
+from typing import Dict
 
 import numpy as np
 import talib.abstract as ta
@@ -43,7 +44,8 @@ class freqai_test_multimodel_classifier_strat(IStrategy):
     )
     max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
 
-    def feature_engineering_expand_all(self, dataframe, period, **kwargs):
+    def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
+                                       metadata: Dict, **kwargs):
 
         dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
         dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
@@ -51,7 +53,7 @@ class freqai_test_multimodel_classifier_strat(IStrategy):
 
         return dataframe
 
-    def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs):
+    def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-pct-change"] = dataframe["close"].pct_change()
         dataframe["%-raw_volume"] = dataframe["volume"]
@@ -59,14 +61,14 @@ class freqai_test_multimodel_classifier_strat(IStrategy):
 
         return dataframe
 
-    def feature_engineering_standard(self, dataframe, **kwargs):
+    def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
         dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
 
         return dataframe
 
-    def set_freqai_targets(self, dataframe, **kwargs):
+    def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) >
                                               dataframe["close"], 'up', 'down')
diff --git a/tests/strategy/strats/freqai_test_multimodel_strat.py b/tests/strategy/strats/freqai_test_multimodel_strat.py
index 5c9712629..5b09598a5 100644
--- a/tests/strategy/strats/freqai_test_multimodel_strat.py
+++ b/tests/strategy/strats/freqai_test_multimodel_strat.py
@@ -1,5 +1,6 @@
 import logging
 from functools import reduce
+from typing import Dict
 
 import talib.abstract as ta
 from pandas import DataFrame
@@ -42,7 +43,8 @@ class freqai_test_multimodel_strat(IStrategy):
     )
     max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
 
-    def feature_engineering_expand_all(self, dataframe, period, **kwargs):
+    def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
+                                       metadata: Dict, **kwargs):
 
         dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
         dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
@@ -50,7 +52,7 @@ class freqai_test_multimodel_strat(IStrategy):
 
         return dataframe
 
-    def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs):
+    def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-pct-change"] = dataframe["close"].pct_change()
         dataframe["%-raw_volume"] = dataframe["volume"]
@@ -58,14 +60,14 @@ class freqai_test_multimodel_strat(IStrategy):
 
         return dataframe
 
-    def feature_engineering_standard(self, dataframe, **kwargs):
+    def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
         dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
 
         return dataframe
 
-    def set_freqai_targets(self, dataframe, **kwargs):
+    def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["&-s_close"] = (
             dataframe["close"]
diff --git a/tests/strategy/strats/freqai_test_strat.py b/tests/strategy/strats/freqai_test_strat.py
index b52c95908..6db308406 100644
--- a/tests/strategy/strats/freqai_test_strat.py
+++ b/tests/strategy/strats/freqai_test_strat.py
@@ -1,5 +1,6 @@
 import logging
 from functools import reduce
+from typing import Dict
 
 import talib.abstract as ta
 from pandas import DataFrame
@@ -42,7 +43,8 @@ class freqai_test_strat(IStrategy):
     )
     max_roi_time_long = IntParameter(0, 800, default=400, space="sell", optimize=False, load=True)
 
-    def feature_engineering_expand_all(self, dataframe, period, **kwargs):
+    def feature_engineering_expand_all(self, dataframe: DataFrame, period: int,
+                                       metadata: Dict, **kwargs):
 
         dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
         dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
@@ -50,7 +52,7 @@ class freqai_test_strat(IStrategy):
 
         return dataframe
 
-    def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs):
+    def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-pct-change"] = dataframe["close"].pct_change()
         dataframe["%-raw_volume"] = dataframe["volume"]
@@ -58,14 +60,14 @@ class freqai_test_strat(IStrategy):
 
         return dataframe
 
-    def feature_engineering_standard(self, dataframe, **kwargs):
+    def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
         dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
 
         return dataframe
 
-    def set_freqai_targets(self, dataframe, **kwargs):
+    def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs):
 
         dataframe["&-s_close"] = (
             dataframe["close"]
diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py
index 9850a5675..d05e8ead2 100644
--- a/tests/strategy/strats/hyperoptable_strategy.py
+++ b/tests/strategy/strats/hyperoptable_strategy.py
@@ -34,6 +34,11 @@ class HyperoptableStrategy(StrategyTestV3):
     protection_enabled = BooleanParameter(default=True)
     protection_cooldown_lookback = IntParameter([0, 50], default=30)
 
+    # Invalid plot config ...
+    plot_config = {
+        "main_plot": {},
+    }
+
     @property
     def protections(self):
         prot = []
@@ -45,6 +50,7 @@ class HyperoptableStrategy(StrategyTestV3):
         return prot
 
     bot_loop_started = False
+    bot_started = False
 
     def bot_loop_start(self):
         self.bot_loop_started = True
@@ -53,6 +59,7 @@ class HyperoptableStrategy(StrategyTestV3):
         """
         Parameters can also be defined here ...
         """
+        self.bot_started = True
         self.buy_rsi = IntParameter([0, 50], default=30, space='buy')
 
     def informative_pairs(self):
diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py
index 088ab21d4..2d5121403 100644
--- a/tests/strategy/strats/strategy_test_v3.py
+++ b/tests/strategy/strats/strategy_test_v3.py
@@ -30,6 +30,9 @@ class StrategyTestV3(IStrategy):
         "0": 0.04
     }
 
+    # Optimal max_open_trades for the strategy
+    max_open_trades = -1
+
     # Optimal stoploss designed for the strategy
     stoploss = -0.10
 
@@ -194,7 +197,7 @@ class StrategyTestV3(IStrategy):
 
         if current_profit < -0.0075:
             orders = trade.select_filled_orders(trade.entry_side)
-            return round(orders[0].cost, 0)
+            return round(orders[0].safe_cost, 0)
 
         return None
 
diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py
index cb3d61e89..5f41177eb 100644
--- a/tests/strategy/test_default_strategy.py
+++ b/tests/strategy/test_default_strategy.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timezone
 
 import pytest
 from pandas import DataFrame
@@ -43,12 +43,12 @@ def test_strategy_test_v3(dataframe_1m, fee, is_short, side):
 
     assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1,
                                         rate=20000, time_in_force='gtc',
-                                        current_time=datetime.utcnow(),
+                                        current_time=datetime.now(timezone.utc),
                                         side=side, entry_tag=None) is True
     assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1,
                                        rate=20000, time_in_force='gtc', exit_reason='roi',
                                        sell_reason='roi',
-                                       current_time=datetime.utcnow(),
+                                       current_time=datetime.now(timezone.utc),
                                        side=side) is True
 
     assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py
index 294021c83..8a609cf30 100644
--- a/tests/strategy/test_interface.py
+++ b/tests/strategy/test_interface.py
@@ -4,11 +4,11 @@ from datetime import datetime, timedelta, timezone
 from pathlib import Path
 from unittest.mock import MagicMock
 
-import arrow
 import pytest
 from pandas import DataFrame
 
 from freqtrade.configuration import TimeRange
+from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH
 from freqtrade.data.dataprovider import DataProvider
 from freqtrade.data.history import load_data
 from freqtrade.enums import ExitCheckTuple, ExitType, HyperoptState, SignalDirection
@@ -21,6 +21,7 @@ from freqtrade.strategy.hyper import detect_parameters
 from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
                                            DecimalParameter, IntParameter, RealParameter)
 from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
+from freqtrade.util import dt_now
 from tests.conftest import (CURRENT_TEST_STRATEGY, TRADE_SIDES, create_mock_trades, log_has,
                             log_has_re)
 
@@ -33,7 +34,7 @@ _STRATEGY.dp = DataProvider({}, None, None)
 
 
 def test_returns_latest_signal(ohlcv_history):
-    ohlcv_history.loc[1, 'date'] = arrow.utcnow()
+    ohlcv_history.loc[1, 'date'] = dt_now()
     # Take a copy to correctly modify the call
     mocked_history = ohlcv_history.copy()
     mocked_history['enter_long'] = 0
@@ -158,7 +159,7 @@ def test_get_signal_exception_valueerror(mocker, caplog, ohlcv_history):
 def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
     # default_conf defines a 5m interval. we check interval * 2 + 5m
     # this is necessary as the last candle is removed (partial candles) by default
-    ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
+    ohlcv_history.loc[1, 'date'] = dt_now() - timedelta(minutes=16)
     # Take a copy to correctly modify the call
     mocked_history = ohlcv_history.copy()
     mocked_history['exit_long'] = 0
@@ -179,7 +180,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
 def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history):
     # default_conf defines a 5m interval. we check interval * 2 + 5m
     # this is necessary as the last candle is removed (partial candles) by default
-    ohlcv_history.loc[1, 'date'] = arrow.utcnow()
+    ohlcv_history.loc[1, 'date'] = dt_now()
     # Take a copy to correctly modify the call
     mocked_history = ohlcv_history.copy()
     # Intentionally don't set sell column
@@ -214,16 +215,16 @@ def test_ignore_expired_candle(default_conf):
 
     current_time = latest_date + timedelta(seconds=30 + 300)
 
-    assert not strategy.ignore_expired_candle(
+    assert strategy.ignore_expired_candle(
         latest_date=latest_date,
         current_time=current_time,
         timeframe_seconds=300,
         enter=True
-    ) is True
+    ) is not True
 
 
 def test_assert_df_raise(mocker, caplog, ohlcv_history):
-    ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16)
+    ohlcv_history.loc[1, 'date'] = dt_now() - timedelta(minutes=16)
     # Take a copy to correctly modify the call
     mocked_history = ohlcv_history.copy()
     mocked_history['sell'] = 0
@@ -291,18 +292,6 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None:
     assert len(processed['UNITTEST/BTC']) == 103
 
 
-def test_populate_any_indicators(default_conf, testdatadir) -> None:
-    strategy = StrategyResolver.load_strategy(default_conf)
-
-    timerange = TimeRange.parse_timerange('1510694220-1510700340')
-    data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange,
-                     fill_up_missing=True)
-    processed = strategy.populate_any_indicators('UNITTEST/BTC', data, '5m')
-    assert processed == data
-    assert id(processed) == id(data)
-    assert len(processed['UNITTEST/BTC']) == 103
-
-
 def test_freqai_not_initialized(default_conf) -> None:
     strategy = StrategyResolver.load_strategy(default_conf)
     strategy.ft_bot_start()
@@ -334,21 +323,21 @@ def test_min_roi_reached(default_conf, fee) -> None:
             pair='ETH/BTC',
             stake_amount=0.001,
             amount=5,
-            open_date=arrow.utcnow().shift(hours=-1).datetime,
+            open_date=dt_now() - timedelta(hours=1),
             fee_open=fee.return_value,
             fee_close=fee.return_value,
             exchange='binance',
             open_rate=1,
         )
 
-        assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
-        assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
+        assert not strategy.min_roi_reached(trade, 0.02, dt_now() - timedelta(minutes=56))
+        assert strategy.min_roi_reached(trade, 0.12, dt_now() - timedelta(minutes=56))
 
-        assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
-        assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
+        assert not strategy.min_roi_reached(trade, 0.04, dt_now() - timedelta(minutes=39))
+        assert strategy.min_roi_reached(trade, 0.06, dt_now() - timedelta(minutes=39))
 
-        assert not strategy.min_roi_reached(trade, -0.01, arrow.utcnow().shift(minutes=-1).datetime)
-        assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
+        assert not strategy.min_roi_reached(trade, -0.01, dt_now() - timedelta(minutes=1))
+        assert strategy.min_roi_reached(trade, 0.02, dt_now() - timedelta(minutes=1))
 
 
 def test_min_roi_reached2(default_conf, fee) -> None:
@@ -372,25 +361,25 @@ def test_min_roi_reached2(default_conf, fee) -> None:
             pair='ETH/BTC',
             stake_amount=0.001,
             amount=5,
-            open_date=arrow.utcnow().shift(hours=-1).datetime,
+            open_date=dt_now() - timedelta(hours=1),
             fee_open=fee.return_value,
             fee_close=fee.return_value,
             exchange='binance',
             open_rate=1,
         )
 
-        assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
-        assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
+        assert not strategy.min_roi_reached(trade, 0.02, dt_now() - timedelta(minutes=56))
+        assert strategy.min_roi_reached(trade, 0.12, dt_now() - timedelta(minutes=56))
 
-        assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
-        assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
+        assert not strategy.min_roi_reached(trade, 0.04, dt_now() - timedelta(minutes=39))
+        assert strategy.min_roi_reached(trade, 0.071, dt_now() - timedelta(minutes=39))
 
-        assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
-        assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
+        assert not strategy.min_roi_reached(trade, 0.04, dt_now() - timedelta(minutes=26))
+        assert strategy.min_roi_reached(trade, 0.06, dt_now() - timedelta(minutes=26))
 
         # Should not trigger with 20% profit since after 55 minutes only 30% is active.
-        assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
-        assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
+        assert not strategy.min_roi_reached(trade, 0.20, dt_now() - timedelta(minutes=2))
+        assert strategy.min_roi_reached(trade, 0.31, dt_now() - timedelta(minutes=2))
 
 
 def test_min_roi_reached3(default_conf, fee) -> None:
@@ -406,25 +395,25 @@ def test_min_roi_reached3(default_conf, fee) -> None:
         pair='ETH/BTC',
         stake_amount=0.001,
         amount=5,
-        open_date=arrow.utcnow().shift(hours=-1).datetime,
+        open_date=dt_now() - timedelta(hours=1),
         fee_open=fee.return_value,
         fee_close=fee.return_value,
         exchange='binance',
         open_rate=1,
     )
 
-    assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
-    assert not strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
+    assert not strategy.min_roi_reached(trade, 0.02, dt_now() - timedelta(minutes=56))
+    assert not strategy.min_roi_reached(trade, 0.12, dt_now() - timedelta(minutes=56))
 
-    assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
-    assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
+    assert not strategy.min_roi_reached(trade, 0.04, dt_now() - timedelta(minutes=39))
+    assert strategy.min_roi_reached(trade, 0.071, dt_now() - timedelta(minutes=39))
 
-    assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
-    assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
+    assert not strategy.min_roi_reached(trade, 0.04, dt_now() - timedelta(minutes=26))
+    assert strategy.min_roi_reached(trade, 0.06, dt_now() - timedelta(minutes=26))
 
     # Should not trigger with 20% profit since after 55 minutes only 30% is active.
-    assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
-    assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
+    assert not strategy.min_roi_reached(trade, 0.20, dt_now() - timedelta(minutes=2))
+    assert strategy.min_roi_reached(trade, 0.31, dt_now() - timedelta(minutes=2))
 
 
 @pytest.mark.parametrize(
@@ -452,15 +441,15 @@ def test_min_roi_reached3(default_conf, fee) -> None:
         (0.05, 0.9, ExitType.NONE, None, False, True, 0.09, 0.9, ExitType.NONE,
          lambda **kwargs: None),
     ])
-def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom,
-                           profit2, adjusted2, expected2, custom_stop) -> None:
+def test_ft_stoploss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom,
+                             profit2, adjusted2, expected2, custom_stop) -> None:
 
     strategy = StrategyResolver.load_strategy(default_conf)
     trade = Trade(
         pair='ETH/BTC',
         stake_amount=0.01,
         amount=1,
-        open_date=arrow.utcnow().shift(hours=-1).datetime,
+        open_date=dt_now() - timedelta(hours=1),
         fee_open=fee.return_value,
         fee_close=fee.return_value,
         exchange='binance',
@@ -475,11 +464,11 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, t
     if custom_stop:
         strategy.custom_stoploss = custom_stop
 
-    now = arrow.utcnow().datetime
+    now = dt_now()
     current_rate = trade.open_rate * (1 + profit)
-    sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade,
-                                         current_time=now, current_profit=profit,
-                                         force_stoploss=0, high=None)
+    sl_flag = strategy.ft_stoploss_reached(current_rate=current_rate, trade=trade,
+                                           current_time=now, current_profit=profit,
+                                           force_stoploss=0, high=None)
     assert isinstance(sl_flag, ExitCheckTuple)
     assert sl_flag.exit_type == expected
     if expected == ExitType.NONE:
@@ -489,9 +478,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, t
     assert round(trade.stop_loss, 2) == adjusted
     current_rate2 = trade.open_rate * (1 + profit2)
 
-    sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade,
-                                         current_time=now, current_profit=profit2,
-                                         force_stoploss=0, high=None)
+    sl_flag = strategy.ft_stoploss_reached(current_rate=current_rate2, trade=trade,
+                                           current_time=now, current_profit=profit2,
+                                           force_stoploss=0, high=None)
     assert sl_flag.exit_type == expected2
     if expected2 == ExitType.NONE:
         assert sl_flag.exit_flag is False
@@ -509,14 +498,14 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
         pair='ETH/BTC',
         stake_amount=0.01,
         amount=1,
-        open_date=arrow.utcnow().shift(hours=-1).datetime,
+        open_date=dt_now() - timedelta(hours=1),
         fee_open=fee.return_value,
         fee_close=fee.return_value,
         exchange='binance',
         open_rate=1,
     )
 
-    now = arrow.utcnow().datetime
+    now = dt_now()
     res = strategy.should_exit(trade, 1, now,
                                enter=False, exit_=False,
                                low=None, high=None)
@@ -541,13 +530,13 @@ def test_custom_exit(default_conf, fee, caplog) -> None:
     assert res[0].exit_reason == 'hello world'
 
     caplog.clear()
-    strategy.custom_exit = MagicMock(return_value='h' * 100)
+    strategy.custom_exit = MagicMock(return_value='h' * CUSTOM_TAG_MAX_LENGTH * 2)
     res = strategy.should_exit(trade, 1, now,
                                enter=False, exit_=False,
                                low=None, high=None)
     assert res[0].exit_type == ExitType.CUSTOM_EXIT
     assert res[0].exit_flag is True
-    assert res[0].exit_reason == 'h' * 64
+    assert res[0].exit_reason == 'h' * (CUSTOM_TAG_MAX_LENGTH)
     assert log_has_re('Custom exit reason returned from custom_exit is too long.*', caplog)
 
 
@@ -558,13 +547,13 @@ def test_should_sell(default_conf, fee) -> None:
         pair='ETH/BTC',
         stake_amount=0.01,
         amount=1,
-        open_date=arrow.utcnow().shift(hours=-1).datetime,
+        open_date=dt_now() - timedelta(hours=1),
         fee_open=fee.return_value,
         fee_close=fee.return_value,
         exchange='binance',
         open_rate=1,
     )
-    now = arrow.utcnow().datetime
+    now = dt_now()
     res = strategy.should_exit(trade, 1, now,
                                enter=False, exit_=False,
                                low=None, high=None)
@@ -579,7 +568,7 @@ def test_should_sell(default_conf, fee) -> None:
     assert res == [ExitCheckTuple(exit_type=ExitType.ROI)]
 
     strategy.min_roi_reached = MagicMock(return_value=True)
-    strategy.stop_loss_reached = MagicMock(
+    strategy.ft_stoploss_reached = MagicMock(
         return_value=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
 
     res = strategy.should_exit(trade, 1, now,
@@ -603,7 +592,7 @@ def test_should_sell(default_conf, fee) -> None:
         ExitCheckTuple(exit_type=ExitType.ROI),
         ]
 
-    strategy.stop_loss_reached = MagicMock(
+    strategy.ft_stoploss_reached = MagicMock(
             return_value=ExitCheckTuple(exit_type=ExitType.TRAILING_STOP_LOSS))
     # Regular exit signal
     res = strategy.should_exit(trade, 1, now,
@@ -739,7 +728,7 @@ def test_is_pair_locked(default_conf):
 
     pair = 'ETH/BTC'
     assert not strategy.is_pair_locked(pair)
-    strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime)
+    strategy.lock_pair(pair, dt_now() + timedelta(minutes=4))
     # ETH/BTC locked for 4 minutes
     assert strategy.is_pair_locked(pair)
 
@@ -757,7 +746,7 @@ def test_is_pair_locked(default_conf):
 
     # Lock with reason
     reason = "TestLockR"
-    strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime, reason)
+    strategy.lock_pair(pair, dt_now() + timedelta(minutes=4), reason)
     assert strategy.is_pair_locked(pair)
     strategy.unlock_reason(reason)
     assert not strategy.is_pair_locked(pair)
@@ -998,7 +987,8 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
             }
         }
     }
-    mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
+    mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
+                 return_value=expected_result)
     PairLocks.timeframe = default_conf['timeframe']
     strategy = StrategyResolver.load_strategy(default_conf)
     assert strategy.stoploss == -0.05
@@ -1017,11 +1007,13 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):
         }
     }
 
-    mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result)
+    mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
+                 return_value=expected_result)
     with pytest.raises(OperationalException, match="Invalid parameter file provided."):
         StrategyResolver.load_strategy(default_conf)
 
-    mocker.patch('freqtrade.strategy.hyper.json_load', MagicMock(side_effect=ValueError()))
+    mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params',
+                 MagicMock(side_effect=ValueError()))
 
     StrategyResolver.load_strategy(default_conf)
     assert log_has("Invalid parameter file format.", caplog)
diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py
index f42f9e681..a55580780 100644
--- a/tests/strategy/test_strategy_helpers.py
+++ b/tests/strategy/test_strategy_helpers.py
@@ -119,53 +119,88 @@ def test_merge_informative_pair_suffix_append_timeframe():
         merge_informative_pair(data, informative, '15m', '1h', suffix="suf")
 
 
-def test_stoploss_from_open():
+@pytest.mark.parametrize("side,profitrange", [
+    # profit range for long is [-1, inf] while for shorts is [-inf, 1]
+    ("long", [-0.99, 2, 30]),
+    ("short", [-2.0, 0.99, 30]),
+])
+def test_stoploss_from_open(side, profitrange):
     open_price_ranges = [
         [0.01, 1.00, 30],
         [1, 100, 30],
         [100, 10000, 30],
     ]
-    # profit range for long is [-1, inf] while for shorts is [-inf, 1]
-    current_profit_range_dict = {'long': [-0.99, 2, 30], 'short': [-2.0, 0.99, 30]}
-    desired_stop_range = [-0.50, 0.50, 30]
 
-    for side, current_profit_range in current_profit_range_dict.items():
-        for open_range in open_price_ranges:
-            for open_price in np.linspace(*open_range):
-                for desired_stop in np.linspace(*desired_stop_range):
+    for open_range in open_price_ranges:
+        for open_price in np.linspace(*open_range):
+            for desired_stop in np.linspace(-0.50, 0.50, 30):
 
+                if side == 'long':
+                    # -1 is not a valid current_profit, should return 1
+                    assert stoploss_from_open(desired_stop, -1) == 1
+                else:
+                    # 1 is not a valid current_profit for shorts, should return 1
+                    assert stoploss_from_open(desired_stop, 1, True) == 1
+
+                for current_profit in np.linspace(*profitrange):
                     if side == 'long':
-                        # -1 is not a valid current_profit, should return 1
-                        assert stoploss_from_open(desired_stop, -1) == 1
+                        current_price = open_price * (1 + current_profit)
+                        expected_stop_price = open_price * (1 + desired_stop)
+                        stoploss = stoploss_from_open(desired_stop, current_profit)
+                        stop_price = current_price * (1 - stoploss)
                     else:
-                        # 1 is not a valid current_profit for shorts, should return 1
-                        assert stoploss_from_open(desired_stop, 1, True) == 1
+                        current_price = open_price * (1 - current_profit)
+                        expected_stop_price = open_price * (1 - desired_stop)
+                        stoploss = stoploss_from_open(desired_stop, current_profit, True)
+                        stop_price = current_price * (1 + stoploss)
 
-                    for current_profit in np.linspace(*current_profit_range):
-                        if side == 'long':
-                            current_price = open_price * (1 + current_profit)
-                            expected_stop_price = open_price * (1 + desired_stop)
-                            stoploss = stoploss_from_open(desired_stop, current_profit)
-                            stop_price = current_price * (1 - stoploss)
-                        else:
-                            current_price = open_price * (1 - current_profit)
-                            expected_stop_price = open_price * (1 - desired_stop)
-                            stoploss = stoploss_from_open(desired_stop, current_profit, True)
-                            stop_price = current_price * (1 + stoploss)
+                    assert stoploss >= 0
+                    # Technically the formula can yield values greater than 1 for shorts
+                    # eventhough it doesn't make sense because the position would be liquidated
+                    if side == 'long':
+                        assert stoploss <= 1
 
-                        assert stoploss >= 0
-                        # Technically the formula can yield values greater than 1 for shorts
-                        # eventhough it doesn't make sense because the position would be liquidated
-                        if side == 'long':
-                            assert stoploss <= 1
+                    # there is no correct answer if the expected stop price is above
+                    # the current price
+                    if ((side == 'long' and expected_stop_price > current_price)
+                            or (side == 'short' and expected_stop_price < current_price)):
+                        assert stoploss == 0
+                    else:
+                        assert pytest.approx(stop_price) == expected_stop_price
 
-                        # there is no correct answer if the expected stop price is above
-                        # the current price
-                        if ((side == 'long' and expected_stop_price > current_price)
-                                or (side == 'short' and expected_stop_price < current_price)):
-                            assert stoploss == 0
-                        else:
-                            assert pytest.approx(stop_price) == expected_stop_price
+
+@pytest.mark.parametrize("side,rel_stop,curr_profit,leverage,expected", [
+    # profit range for long is [-1, inf] while for shorts is [-inf, 1]
+    ("long", 0, -1, 1, 1),
+    ("long", 0, 0.1, 1, 0.09090909),
+    ("long", -0.1, 0.1, 1, 0.18181818),
+    ("long", 0.1, 0.2, 1, 0.08333333),
+    ("long", 0.1, 0.5, 1, 0.266666666),
+    ("long", 0.1, 5, 1, 0.816666666),  # 500% profit, set stoploss to 10% above open price
+    ("long", 0, 5, 10,  3.3333333),  # 500% profit, set stoploss break even
+    ("long", 0.1, 5, 10,  3.26666666),  # 500% profit, set stoploss to 10% above open price
+    ("long", -0.1, 5, 10,  3.3999999),  # 500% profit, set stoploss to 10% belowopen price
+
+    ("short", 0, 0.1, 1, 0.1111111),
+    ("short", -0.1, 0.1, 1, 0.2222222),
+    ("short", 0.1, 0.2, 1, 0.125),
+    ("short", 0.1, 1, 1, 1),
+    ("short", -0.01, 5, 10, 10.01999999),  # 500% profit at 10x
+])
+def test_stoploss_from_open_leverage(side, rel_stop, curr_profit, leverage, expected):
+
+    stoploss = stoploss_from_open(rel_stop, curr_profit, side == 'short', leverage)
+    assert pytest.approx(stoploss) == expected
+    open_rate = 100
+    if stoploss != 1:
+        if side == 'long':
+            current_rate = open_rate * (1 + curr_profit / leverage)
+            stop = current_rate * (1 - stoploss / leverage)
+            assert pytest.approx(stop) == open_rate * (1 + rel_stop / leverage)
+        else:
+            current_rate = open_rate * (1 - curr_profit / leverage)
+            stop = current_rate * (1 + stoploss / leverage)
+            assert pytest.approx(stop) == open_rate * (1 - rel_stop / leverage)
 
 
 def test_stoploss_from_absolute():
diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py
index 5fcc75026..4cdb35936 100644
--- a/tests/strategy/test_strategy_loading.py
+++ b/tests/strategy/test_strategy_loading.py
@@ -6,6 +6,7 @@ from pathlib import Path
 import pytest
 from pandas import DataFrame
 
+from freqtrade.configuration import Configuration
 from freqtrade.exceptions import OperationalException
 from freqtrade.resolvers import StrategyResolver
 from freqtrade.strategy.interface import IStrategy
@@ -68,7 +69,7 @@ def test_load_strategy(default_conf, dataframe_1m):
 def test_load_strategy_base64(dataframe_1m, caplog, default_conf):
     filepath = Path(__file__).parents[2] / 'freqtrade/templates/sample_strategy.py'
     encoded_string = urlsafe_b64encode(filepath.read_bytes()).decode("utf-8")
-    default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)})
+    default_conf.update({'strategy': f'SampleStrategy:{encoded_string}'})
 
     strategy = StrategyResolver.load_strategy(default_conf)
     assert 'rsi' in strategy.advise_indicators(dataframe_1m, {'pair': 'ETH/BTC'})
@@ -175,6 +176,18 @@ def test_strategy_override_stoploss(caplog, default_conf):
     assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
 
 
+def test_strategy_override_max_open_trades(caplog, default_conf):
+    caplog.set_level(logging.INFO)
+    default_conf.update({
+        'strategy': CURRENT_TEST_STRATEGY,
+        'max_open_trades': 7
+    })
+    strategy = StrategyResolver.load_strategy(default_conf)
+
+    assert strategy.max_open_trades == 7
+    assert log_has("Override strategy 'max_open_trades' with value in config file: 7.", caplog)
+
+
 def test_strategy_override_trailing_stop(caplog, default_conf):
     caplog.set_level(logging.INFO)
     default_conf.update({
@@ -349,6 +362,38 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf):
     assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
 
 
+def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf):
+    caplog.set_level(logging.INFO)
+    default_conf.update({
+        'strategy': CURRENT_TEST_STRATEGY,
+    })
+    del default_conf['max_open_trades']
+
+    strategy = StrategyResolver.load_strategy(default_conf)
+
+    # this test assumes -1 set to 'max_open_trades' in CURRENT_TEST_STRATEGY
+    assert strategy.max_open_trades == float('inf')
+    assert default_conf['max_open_trades'] == float('inf')
+
+
+def test_strategy_max_open_trades_infinity_from_config(caplog, default_conf, mocker):
+    caplog.set_level(logging.INFO)
+    default_conf.update({
+        'strategy': CURRENT_TEST_STRATEGY,
+        'max_open_trades': -1,
+        'exchange': 'binance'
+    })
+
+    configuration = Configuration(args=default_conf)
+    parsed_config = configuration.get_config()
+
+    assert parsed_config['max_open_trades'] == float('inf')
+
+    strategy = StrategyResolver.load_strategy(parsed_config)
+
+    assert strategy.max_open_trades == float('inf')
+
+
 @ pytest.mark.filterwarnings("ignore:deprecated")
 def test_missing_implements(default_conf, caplog):
 
@@ -438,3 +483,19 @@ def test_strategy_interface_versioning(dataframe_1m, default_conf):
     assert isinstance(exitdf, DataFrame)
     assert 'sell' not in exitdf
     assert 'exit_long' in exitdf
+
+
+def test_strategy_ft_load_params_from_file(mocker, default_conf):
+    default_conf.update({'strategy': 'StrategyTestV2'})
+    del default_conf['max_open_trades']
+    mocker.patch('freqtrade.strategy.hyper.HyperStrategyMixin.load_params_from_file',
+                 return_value={
+                     'params': {
+                         'max_open_trades':  {
+                            'max_open_trades': -1
+                         }
+                         }
+                     })
+    strategy = StrategyResolver.load_strategy(default_conf)
+    assert strategy.max_open_trades == float('inf')
+    assert strategy.config['max_open_trades'] == float('inf')
diff --git a/tests/test_binance_mig.py b/tests/test_binance_mig.py
new file mode 100644
index 000000000..5a5bbe9dc
--- /dev/null
+++ b/tests/test_binance_mig.py
@@ -0,0 +1,58 @@
+
+
+import shutil
+from pathlib import Path
+
+import pytest
+
+from freqtrade.persistence import Trade
+from freqtrade.util.binance_mig import migrate_binance_futures_data, migrate_binance_futures_names
+from tests.conftest import create_mock_trades_usdt, log_has
+
+
+def test_binance_mig_data_conversion(default_conf_usdt, tmpdir, testdatadir):
+
+    # call doing nothing (spot mode)
+    migrate_binance_futures_data(default_conf_usdt)
+    default_conf_usdt['trading_mode'] = 'futures'
+    pair_old = 'XRP_USDT'
+    pair_unified = 'XRP_USDT_USDT'
+    futures_src = testdatadir / 'futures'
+    futures_dst = tmpdir / 'futures'
+    futures_dst.mkdir()
+    files = [
+        '-1h-mark.json',
+        '-1h-futures.json',
+        '-8h-funding_rate.json',
+        '-8h-mark.json',
+    ]
+
+    # Copy files to tmpdir and rename to old naming
+    for file in files:
+        fn_after = futures_dst / f'{pair_old}{file}'
+        shutil.copy(futures_src / f'{pair_unified}{file}', fn_after)
+
+    default_conf_usdt['datadir'] = Path(tmpdir)
+    # Migrate files to unified namings
+    migrate_binance_futures_data(default_conf_usdt)
+
+    for file in files:
+        fn_after = futures_dst / f'{pair_unified}{file}'
+        assert fn_after.exists()
+
+
+@pytest.mark.usefixtures("init_persistence")
+def test_binance_mig_db_conversion(default_conf_usdt, fee, caplog):
+    # Does nothing in spot mode
+    migrate_binance_futures_names(default_conf_usdt)
+
+    create_mock_trades_usdt(fee, None)
+
+    for t in Trade.get_trades():
+        t.trading_mode = 'FUTURES'
+        t.exchange = 'binance'
+    Trade.commit()
+
+    default_conf_usdt['trading_mode'] = 'futures'
+    migrate_binance_futures_names(default_conf_usdt)
+    assert log_has('Migrating binance futures pairs in database.', caplog)
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
index cdf9f2f2e..5b09abbd3 100644
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -23,7 +23,8 @@ from freqtrade.configuration.load_config import (load_config_file, load_file, lo
 from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
 from freqtrade.enums import RunMode
 from freqtrade.exceptions import OperationalException
-from freqtrade.loggers import FTBufferingHandler, _set_loggers, setup_logging, setup_logging_pre
+from freqtrade.loggers import (FTBufferingHandler, FTStdErrStreamHandler, _set_loggers,
+                               setup_logging, setup_logging_pre)
 from tests.conftest import (CURRENT_TEST_STRATEGY, log_has, log_has_re,
                             patched_configuration_load_config_file)
 
@@ -58,7 +59,8 @@ def test_load_config_incorrect_stake_amount(default_conf) -> None:
 
 def test_load_config_file(default_conf, mocker, caplog) -> None:
     del default_conf['user_data_dir']
-    file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
+    default_conf['datadir'] = str(default_conf['datadir'])
+    file_mock = mocker.patch('freqtrade.configuration.load_config.Path.open', mocker.mock_open(
         read_data=json.dumps(default_conf)
     ))
 
@@ -69,9 +71,11 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
 
 def test_load_config_file_error(default_conf, mocker, caplog) -> None:
     del default_conf['user_data_dir']
+    default_conf['datadir'] = str(default_conf['datadir'])
     filedata = json.dumps(default_conf).replace(
         '"stake_amount": 0.001,', '"stake_amount": .001,')
-    mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(read_data=filedata))
+    mocker.patch('freqtrade.configuration.load_config.Path.open',
+                 mocker.mock_open(read_data=filedata))
     mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
 
     with pytest.raises(OperationalException, match=r".*Please verify the following segment.*"):
@@ -80,6 +84,7 @@ def test_load_config_file_error(default_conf, mocker, caplog) -> None:
 
 def test_load_config_file_error_range(default_conf, mocker, caplog) -> None:
     del default_conf['user_data_dir']
+    default_conf['datadir'] = str(default_conf['datadir'])
     filedata = json.dumps(default_conf).replace(
         '"stake_amount": 0.001,', '"stake_amount": .001,')
     mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata))
@@ -238,6 +243,7 @@ def test_print_config(default_conf, mocker, caplog) -> None:
     conf1 = deepcopy(default_conf)
     # Delete non-json elements from default_conf
     del conf1['user_data_dir']
+    conf1['datadir'] = str(conf1['datadir'])
     config_files = [conf1]
 
     configsmock = MagicMock(side_effect=config_files)
@@ -268,7 +274,7 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
 
 def test_load_config_file_exception(mocker) -> None:
     mocker.patch(
-        'freqtrade.configuration.configuration.open',
+        'freqtrade.configuration.configuration.Path.open',
         MagicMock(side_effect=FileNotFoundError('File not found'))
     )
 
@@ -653,7 +659,7 @@ def test_set_loggers_syslog():
     setup_logging(config)
     assert len(logger.handlers) == 3
     assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler]
-    assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
+    assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
     assert [x for x in logger.handlers if type(x) == FTBufferingHandler]
     # setting up logging again should NOT cause the loggers to be added a second time.
     setup_logging(config)
@@ -676,7 +682,7 @@ def test_set_loggers_Filehandler(tmpdir):
     setup_logging(config)
     assert len(logger.handlers) == 3
     assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler]
-    assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
+    assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
     assert [x for x in logger.handlers if type(x) == FTBufferingHandler]
     # setting up logging again should NOT cause the loggers to be added a second time.
     setup_logging(config)
@@ -697,15 +703,16 @@ def test_set_loggers_journald(mocker):
               'logfile': 'journald',
               }
 
+    setup_logging_pre()
     setup_logging(config)
-    assert len(logger.handlers) == 2
+    assert len(logger.handlers) == 3
     assert [x for x in logger.handlers if type(x).__name__ == "JournaldLogHandler"]
-    assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
+    assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler]
     # reset handlers to not break pytest
     logger.handlers = orig_handlers
 
 
-def test_set_loggers_journald_importerror(mocker, import_fails):
+def test_set_loggers_journald_importerror(import_fails):
     logger = logging.getLogger()
     orig_handlers = logger.handlers
     logger.handlers = []
@@ -714,7 +721,7 @@ def test_set_loggers_journald_importerror(mocker, import_fails):
               'logfile': 'journald',
               }
     with pytest.raises(OperationalException,
-                       match=r'You need the systemd python package.*'):
+                       match=r'You need the cysystemd python package.*'):
         setup_logging(config)
     logger.handlers = orig_handlers
 
@@ -1264,7 +1271,7 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf):
         configuration.get_config()
 
 
-def test_pairlist_resolving_fallback(mocker):
+def test_pairlist_resolving_fallback(mocker, tmpdir):
     mocker.patch.object(Path, "exists", MagicMock(return_value=True))
     mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock()))
     mocker.patch("freqtrade.configuration.configuration.load_file",
@@ -1283,7 +1290,7 @@ def test_pairlist_resolving_fallback(mocker):
 
     assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
     assert config['exchange']['name'] == 'binance'
-    assert config['datadir'] == Path.cwd() / "user_data/data/binance"
+    assert config['datadir'] == Path(tmpdir) / "user_data/data/binance"
 
 
 @pytest.mark.parametrize("setting", [
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 7efd0393d..71f494372 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -4,12 +4,13 @@
 import logging
 import time
 from copy import deepcopy
+from datetime import timedelta
 from typing import List
 from unittest.mock import ANY, MagicMock, PropertyMock, patch
 
-import arrow
 import pytest
 from pandas import DataFrame
+from sqlalchemy import select
 
 from freqtrade.constants import CANCEL_REASON, UNLIMITED_STAKE_AMOUNT
 from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, RPCMessageType, RunMode,
@@ -21,10 +22,12 @@ from freqtrade.freqtradebot import FreqtradeBot
 from freqtrade.persistence import Order, PairLocks, Trade
 from freqtrade.persistence.models import PairLock
 from freqtrade.plugins.protections.iprotection import ProtectionReturn
+from freqtrade.util.datetime_helpers import dt_now, dt_utc
 from freqtrade.worker import Worker
-from tests.conftest import (create_mock_trades, create_mock_trades_usdt, get_patched_freqtradebot,
-                            get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange,
-                            patch_get_signal, patch_wallet, patch_whitelist)
+from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt,
+                            get_patched_freqtradebot, get_patched_worker, log_has, log_has_re,
+                            patch_edge, patch_exchange, patch_get_signal, patch_wallet,
+                            patch_whitelist)
 from tests.conftest_trades import (MOCK_TRADE_COUNT, entry_side, exit_side, mock_order_1,
                                    mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell,
                                    mock_order_4, mock_order_5_stoploss, mock_order_6_sell)
@@ -46,7 +49,7 @@ def patch_RPCManager(mocker) -> MagicMock:
 
 
 def test_freqtradebot_state(mocker, default_conf_usdt, markets) -> None:
-    mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
+    mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     assert freqtrade.state is State.RUNNING
 
@@ -119,7 +122,7 @@ def test_order_dict(default_conf_usdt, mocker, runmode, caplog) -> None:
 
     freqtrade = FreqtradeBot(conf)
     if runmode == RunMode.LIVE:
-        assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
+        assert not log_has_re(r".*stoploss_on_exchange .* dry-run", caplog)
     assert freqtrade.strategy.order_types['stoploss_on_exchange']
 
     caplog.clear()
@@ -134,7 +137,7 @@ def test_order_dict(default_conf_usdt, mocker, runmode, caplog) -> None:
     }
     freqtrade = FreqtradeBot(conf)
     assert not freqtrade.strategy.order_types['stoploss_on_exchange']
-    assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
+    assert not log_has_re(r".*stoploss_on_exchange .* dry-run", caplog)
 
 
 def test_get_trade_stake_amount(default_conf_usdt, mocker) -> None:
@@ -147,6 +150,34 @@ def test_get_trade_stake_amount(default_conf_usdt, mocker) -> None:
     assert result == default_conf_usdt['stake_amount']
 
 
+@pytest.mark.parametrize('runmode', [
+    RunMode.DRY_RUN,
+    RunMode.LIVE
+])
+def test_load_strategy_no_keys(default_conf_usdt, mocker, runmode, caplog) -> None:
+    patch_RPCManager(mocker)
+    patch_exchange(mocker)
+    conf = deepcopy(default_conf_usdt)
+    conf['runmode'] = runmode
+    erm = mocker.patch('freqtrade.freqtradebot.ExchangeResolver.load_exchange')
+
+    freqtrade = FreqtradeBot(conf)
+    strategy_config = freqtrade.strategy.config
+    assert id(strategy_config['exchange']) == id(conf['exchange'])
+    # Keys have been removed and are not passed to the exchange
+    assert strategy_config['exchange']['key'] == ''
+    assert strategy_config['exchange']['secret'] == ''
+
+    assert erm.call_count == 1
+    ex_conf = erm.call_args_list[0][1]['exchange_config']
+    assert id(ex_conf) != id(conf['exchange'])
+    # Keys are still present
+    assert ex_conf['key'] != ''
+    assert ex_conf['key'] == default_conf_usdt['exchange']['key']
+    assert ex_conf['secret'] != ''
+    assert ex_conf['secret'] == default_conf_usdt['exchange']['secret']
+
+
 @pytest.mark.parametrize("amend_last,wallet,max_open,lsamr,expected", [
                         (False, 120, 2, 0.5, [60, None]),
                         (True, 120, 2, 0.5, [60, 58.8]),
@@ -164,7 +195,7 @@ def test_check_available_stake_amount(
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=limit_buy_order_usdt_open),
         get_fee=fee
@@ -234,7 +265,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
             'last': enter_price,
         }
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value=ticker_val),
         get_fee=fee,
     )
@@ -246,7 +277,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
     patch_get_signal(freqtrade)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     caplog.clear()
     #############################################
     ticker_val.update({
@@ -269,15 +300,15 @@ def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -
     patch_exchange(mocker)
     default_conf_usdt['max_open_trades'] = 2
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
-        _is_dry_limit_order_filled=MagicMock(return_value=False),
+        _dry_is_price_crossed=MagicMock(return_value=False),
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
     patch_get_signal(freqtrade)
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
 
     assert trade is not None
     assert trade.stake_amount == 60.0
@@ -285,7 +316,7 @@ def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -
     assert trade.open_date is not None
 
     freqtrade.enter_positions()
-    trade = Trade.query.order_by(Trade.id.desc()).first()
+    trade = Trade.session.scalars(select(Trade).order_by(Trade.id.desc())).first()
 
     assert trade is not None
     assert trade.stake_amount == 60.0
@@ -304,10 +335,10 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_order,
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
-        _is_dry_limit_order_filled=MagicMock(return_value=False),
+        _dry_is_price_crossed=MagicMock(return_value=False),
     )
 
     # Save state of current whitelist
@@ -316,7 +347,7 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_order,
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.create_trade('ETH/USDT')
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade is not None
     assert pytest.approx(trade.stake_amount) == 60.0
@@ -340,7 +371,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke
     patch_exchange(mocker)
     patch_wallet(mocker, free=default_conf_usdt['stake_amount'] * 0.5)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
     )
@@ -354,7 +385,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke
 @pytest.mark.parametrize("is_short", [False, True])
 @pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
     (5.0, True, True, 99),
-    (0.049, True, False, 99),  # Amount will be adjusted to min - which is 0.051
+    (0.042, True, False, 99),  # Amount will be adjusted to min - which is 0.051
     (0, False, True, 99),
     (UNLIMITED_STAKE_AMOUNT, False, True, 0),
 ])
@@ -366,7 +397,7 @@ def test_create_trade_minimal_amount(
     patch_exchange(mocker)
     enter_mock = MagicMock(return_value=limit_order_open[entry_side(is_short)])
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=enter_mock,
         get_fee=fee,
@@ -401,7 +432,7 @@ def test_enter_positions_no_pairs_left(default_conf_usdt, ticker_usdt, limit_buy
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=limit_buy_order_usdt_open),
         get_fee=fee,
@@ -428,7 +459,7 @@ def test_enter_positions_global_pairlock(default_conf_usdt, ticker_usdt, limit_b
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value={'id': limit_buy_order_usdt['id']}),
         get_fee=fee,
@@ -443,7 +474,7 @@ def test_enter_positions_global_pairlock(default_conf_usdt, ticker_usdt, limit_b
     assert not log_has_re(message, caplog)
     caplog.clear()
 
-    PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because', side='*')
+    PairLocks.lock_pair('*', dt_now() + timedelta(minutes=20), 'Just because', side='*')
     n = freqtrade.enter_positions()
     assert n == 0
     assert log_has_re(message, caplog)
@@ -464,7 +495,7 @@ def test_handle_protections(mocker, default_conf_usdt, fee, is_short):
 
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     freqtrade.protections._protection_handlers[1].global_stop = MagicMock(
-        return_value=ProtectionReturn(True, arrow.utcnow().shift(hours=1).datetime, "asdf"))
+        return_value=ProtectionReturn(True, dt_now() + timedelta(hours=1), "asdf"))
     create_mock_trades(fee, is_short)
     freqtrade.handle_protections('ETC/BTC', '*')
     send_msg_mock = freqtrade.rpc.send_msg
@@ -479,7 +510,7 @@ def test_create_trade_no_signal(default_conf_usdt, fee, mocker) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_fee=fee,
     )
     default_conf_usdt['stake_amount'] = 10
@@ -502,7 +533,7 @@ def test_create_trades_multiple_trades(
     default_conf_usdt['dry_run_wallet'] = 60.0 * max_open
 
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=limit_buy_order_usdt_open),
         get_fee=fee,
@@ -524,7 +555,7 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker,
     patch_exchange(mocker)
     default_conf_usdt['max_open_trades'] = 4
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=limit_buy_order_usdt_open),
         get_fee=fee,
@@ -558,7 +589,7 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=limit_order_open[entry_side(is_short)]),
         fetch_order=MagicMock(return_value=limit_order[entry_side(is_short)]),
@@ -567,12 +598,12 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim
     freqtrade = FreqtradeBot(default_conf_usdt)
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
-    trades = Trade.query.filter(Trade.is_open.is_(True)).all()
+    trades = Trade.get_open_trades()
     assert not trades
 
     freqtrade.process()
 
-    trades = Trade.query.filter(Trade.is_open.is_(True)).all()
+    trades = Trade.get_open_trades()
     assert len(trades) == 1
     trade = trades[0]
     assert trade is not None
@@ -594,7 +625,7 @@ def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> No
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(side_effect=TemporaryError)
     )
@@ -611,7 +642,7 @@ def test_process_operational_exception(default_conf_usdt, ticker_usdt, mocker) -
     msg_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(side_effect=OperationalException)
     )
@@ -630,7 +661,7 @@ def test_process_trade_handling(default_conf_usdt, ticker_usdt, limit_buy_order_
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=limit_buy_order_usdt_open),
         fetch_order=MagicMock(return_value=limit_buy_order_usdt_open),
@@ -639,11 +670,11 @@ def test_process_trade_handling(default_conf_usdt, ticker_usdt, limit_buy_order_
     freqtrade = FreqtradeBot(default_conf_usdt)
     patch_get_signal(freqtrade)
 
-    trades = Trade.query.filter(Trade.is_open.is_(True)).all()
+    trades = Trade.get_open_trades()
     assert not trades
     freqtrade.process()
 
-    trades = Trade.query.filter(Trade.is_open.is_(True)).all()
+    trades = Trade.get_open_trades()
     assert len(trades) == 1
 
     # Nothing happened ...
@@ -657,7 +688,7 @@ def test_process_trade_no_whitelist_pair(default_conf_usdt, ticker_usdt, limit_b
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value={'id': limit_buy_order_usdt['id']}),
         fetch_order=MagicMock(return_value=limit_buy_order_usdt),
@@ -670,7 +701,7 @@ def test_process_trade_no_whitelist_pair(default_conf_usdt, ticker_usdt, limit_b
     assert pair not in default_conf_usdt['exchange']['pair_whitelist']
 
     # create open trade not in whitelist
-    Trade.query.session.add(Trade(
+    Trade.session.add(Trade(
         pair=pair,
         stake_amount=0.001,
         fee_open=fee.return_value,
@@ -680,7 +711,7 @@ def test_process_trade_no_whitelist_pair(default_conf_usdt, ticker_usdt, limit_b
         open_rate=0.01,
         exchange='binance',
     ))
-    Trade.query.session.add(Trade(
+    Trade.session.add(Trade(
         pair='ETH/USDT',
         stake_amount=0.001,
         fee_open=fee.return_value,
@@ -705,7 +736,7 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
 
     refresh_mock = MagicMock()
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(side_effect=TemporaryError),
         refresh_latest_ohlcv=refresh_mock,
@@ -737,20 +768,22 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
 @pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_buffer,liq_price", [
     (False, 'spot', 'binance', None, 0.0, None),
     (True, 'spot', 'binance', None, 0.0, None),
-    (False, 'spot', 'gateio', None, 0.0, None),
-    (True, 'spot', 'gateio', None, 0.0, None),
+    (False, 'spot', 'gate', None, 0.0, None),
+    (True, 'spot', 'gate', None, 0.0, None),
     (False, 'spot', 'okx', None, 0.0, None),
     (True, 'spot', 'okx', None, 0.0, None),
     (True, 'futures', 'binance', 'isolated', 0.0, 11.88151815181518),
     (False, 'futures', 'binance', 'isolated', 0.0, 8.080471380471382),
-    (True, 'futures', 'gateio', 'isolated', 0.0, 11.87413417771621),
-    (False, 'futures', 'gateio', 'isolated', 0.0, 8.085708510208207),
+    (True, 'futures', 'gate', 'isolated', 0.0, 11.87413417771621),
+    (False, 'futures', 'gate', 'isolated', 0.0, 8.085708510208207),
     (True, 'futures', 'binance', 'isolated', 0.05, 11.7874422442244),
     (False, 'futures', 'binance', 'isolated', 0.05, 8.17644781144781),
-    (True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304),
-    (False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796),
+    (True, 'futures', 'gate', 'isolated', 0.05, 11.7804274688304),
+    (False, 'futures', 'gate', 'isolated', 0.05, 8.181423084697796),
     (True, 'futures', 'okx', 'isolated', 0.0, 11.87413417771621),
     (False, 'futures', 'okx', 'isolated', 0.0, 8.085708510208207),
+    (True, 'futures', 'bybit', 'isolated', 0.0, 11.9),
+    (False, 'futures', 'bybit', 'isolated', 0.0, 8.1),
 ])
 def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
                        limit_order_open, is_short, trading_mode,
@@ -766,11 +799,11 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
         ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
         ((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071
 
-    exchange_name = gateio/okx, is_short = true
+    exchange_name = gate/okx, is_short = true
         (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate))
         (10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621
 
-    exchange_name = gateio/okx, is_short = false
+    exchange_name = gate/okx, is_short = false
         (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate))
         (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207
     """
@@ -783,7 +816,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     default_conf_usdt['exchange']['name'] = exchange_name
     if margin_mode:
         default_conf_usdt['margin_mode'] = margin_mode
-    mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes')
+    mocker.patch('freqtrade.exchange.gate.Gate.validate_ordertypes')
     patch_RPCManager(mocker)
     patch_exchange(mocker, id=exchange_name)
     freqtrade = FreqtradeBot(default_conf_usdt)
@@ -794,7 +827,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     enter_rate_mock = MagicMock(return_value=bid)
     enter_mm = MagicMock(return_value=open_order)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_rate=enter_rate_mock,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
@@ -811,7 +844,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
         get_max_leverage=MagicMock(return_value=10),
     )
     mocker.patch.multiple(
-        'freqtrade.exchange.Okx',
+        'freqtrade.exchange.okx.Okx',
         get_max_pair_stake_amount=MagicMock(return_value=500000),
     )
     pair = 'ETH/USDT'
@@ -835,7 +868,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
 
     # Should create an open trade with an open order id
     # As the order is not fulfilled yet
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
     assert trade.is_open is True
@@ -860,10 +893,9 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     order['cost'] = 300
     order['id'] = '444'
 
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=order))
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=order))
     assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
-    trade = Trade.query.all()[2]
+    trade = Trade.session.scalars(select(Trade)).all()[2]
     trade.is_short = is_short
     assert trade
     assert trade.open_order_id is None
@@ -879,10 +911,9 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     order['average'] = 0.5
     order['cost'] = 10.0
     order['id'] = '555'
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=order))
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=order))
     assert freqtrade.execute_entry(pair, stake_amount)
-    trade = Trade.query.all()[3]
+    trade = Trade.session.scalars(select(Trade)).all()[3]
     trade.is_short = is_short
     assert trade
     assert trade.open_order_id is None
@@ -895,7 +926,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
 
     freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0
     assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
-    trade = Trade.query.all()[4]
+    trade = Trade.session.scalars(select(Trade)).all()[4]
     trade.is_short = is_short
     assert trade
     assert pytest.approx(trade.stake_amount) == 150
@@ -904,7 +935,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     order['id'] = '557'
     freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
     assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
-    trade = Trade.query.all()[5]
+    trade = Trade.session.scalars(select(Trade)).all()[5]
     trade.is_short = is_short
     assert trade
     assert pytest.approx(trade.stake_amount) == 2.0
@@ -917,24 +948,23 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     order['average'] = 0.5
     order['cost'] = 0.0
     order['id'] = '66'
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=order))
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=order))
     assert not freqtrade.execute_entry(pair, stake_amount)
     assert freqtrade.strategy.leverage.call_count == 0 if trading_mode == 'spot' else 2
 
     # Fail to get price...
-    mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0))
+    mocker.patch(f'{EXMS}.get_rate', MagicMock(return_value=0.0))
 
     with pytest.raises(PricingError, match="Could not determine entry price."):
         freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
 
     # In case of custom entry price
-    mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.50)
+    mocker.patch(f'{EXMS}.get_rate', return_value=0.50)
     order['status'] = 'open'
     order['id'] = '5566'
     freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.508
     assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
-    trade = Trade.query.all()[6]
+    trade = Trade.session.scalars(select(Trade)).all()[6]
     trade.is_short = is_short
     assert trade
     assert trade.open_rate_requested == 0.508
@@ -946,12 +976,12 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     freqtrade.strategy.custom_entry_price = lambda **kwargs: None
 
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_rate=MagicMock(return_value=10),
     )
 
     assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
-    trade = Trade.query.all()[7]
+    trade = Trade.session.scalars(select(Trade)).all()[7]
     trade.is_short = is_short
     assert trade
     assert trade.open_rate_requested == 10
@@ -961,7 +991,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     order['id'] = '5568'
     freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
     assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
-    trade = Trade.query.all()[8]
+    trade = Trade.session.scalars(select(Trade)).all()[8]
     # Trade(id=9, pair=ETH/USDT, amount=0.20000000, is_short=False,
     #   leverage=1.0, open_rate=10.00000000, open_since=...)
     # Trade(id=9, pair=ETH/USDT, amount=0.60000000, is_short=True,
@@ -976,13 +1006,13 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     order['id'] = '55672'
 
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_max_pair_stake_amount=MagicMock(return_value=500),
     )
     freqtrade.exchange.get_max_pair_stake_amount = MagicMock(return_value=500)
 
     assert freqtrade.execute_entry(pair, 2000, is_short=is_short)
-    trade = Trade.query.all()[9]
+    trade = Trade.session.scalars(select(Trade)).all()[9]
     trade.is_short = is_short
     assert pytest.approx(trade.stake_amount) == 500
 
@@ -991,7 +1021,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
     freqtrade.strategy.leverage.reset_mock()
     assert freqtrade.execute_entry(pair, 200, leverage_=3)
     assert freqtrade.strategy.leverage.call_count == 0
-    trade = Trade.query.all()[10]
+    trade = Trade.session.scalars(select(Trade)).all()[10]
     assert trade.leverage == 1 if trading_mode == 'spot' else 3
 
 
@@ -999,7 +1029,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
 def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order, is_short) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -1034,7 +1064,7 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order,
     default_conf_usdt['margin_mode'] = 'isolated'
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -1053,28 +1083,40 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order,
     freqtrade.strategy.leverage = MagicMock(return_value=5.0)
 
     assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade.leverage == 5.0
     # assert trade.stake_amount == 2
 
 
 @pytest.mark.parametrize("is_short", [False, True])
-def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None:
+def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short, fee) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
+    mocker.patch.multiple(
+        EXMS,
+        fetch_ticker=MagicMock(return_value={
+            'bid': 1.9,
+            'ask': 2.2,
+            'last': 1.9
+        }),
+        create_order=MagicMock(return_value=limit_order[entry_side(is_short)]),
+        get_fee=fee,
+    )
     order = limit_order[entry_side(is_short)]
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
+    mocker.patch(f'{EXMS}.fetch_order', return_value=order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
 
     stoploss = MagicMock(return_value={'id': 13434334})
-    mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
+    mocker.patch(f'{EXMS}.create_stoploss', stoploss)
 
     freqtrade = FreqtradeBot(default_conf_usdt)
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
 
-    # TODO: should not be magicmock
-    trade = MagicMock()
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
+
+    freqtrade.enter_positions()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.open_order_id = None
     trade.stoploss_order_id = None
@@ -1090,13 +1132,14 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho
 @pytest.mark.parametrize("is_short", [False, True])
 def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short,
                                      limit_order) -> None:
-    stoploss = MagicMock(return_value={'id': 13434334})
+    stop_order_dict = {'id': "13434334"}
+    stoploss = MagicMock(return_value=stop_order_dict)
     enter_order = limit_order[entry_side(is_short)]
     exit_order = limit_order[exit_side(is_short)]
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -1107,7 +1150,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
             exit_order,
         ]),
         get_fee=fee,
-        stoploss=stoploss
+        create_stoploss=stoploss
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
@@ -1115,8 +1158,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
     # First case: when stoploss is not yet set but the order is open
     # should get the stoploss order id immediately
     # and should return false as no trade actually happened
-    # TODO: should not be magicmock
-    trade = MagicMock()
+
+    freqtrade.enter_positions()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
@@ -1128,44 +1172,62 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
 
     # Second case: when stoploss is set but it is not yet hit
     # should do nothing and return false
+    stop_order_dict.update({'id': "102"})
     trade.is_open = True
     trade.open_order_id = None
-    trade.stoploss_order_id = "100"
+    trade.stoploss_order_id = "102"
+    trade.orders.append(
+        Order(
+            ft_order_side='stoploss',
+            ft_pair=trade.pair,
+            ft_is_open=True,
+            ft_amount=trade.amount,
+            ft_price=trade.stop_loss,
+            order_id='102',
+            status='open',
+        )
+    )
 
     hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
-    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order)
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order)
 
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
-    assert trade.stoploss_order_id == "100"
+    assert trade.stoploss_order_id == "102"
 
     # Third case: when stoploss was set but it was canceled for some reason
     # should set a stoploss immediately and return False
     caplog.clear()
     trade.is_open = True
     trade.open_order_id = None
-    trade.stoploss_order_id = "100"
+    trade.stoploss_order_id = "102"
 
-    canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
-    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order)
+    canceled_stoploss_order = MagicMock(return_value={'id': '103_1', 'status': 'canceled'})
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order)
     stoploss.reset_mock()
+    amount_before = trade.amount
+
+    stop_order_dict.update({'id': "103_1"})
 
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
     assert stoploss.call_count == 1
-    assert trade.stoploss_order_id == "13434334"
+    assert trade.stoploss_order_id == "103_1"
+    assert trade.amount == amount_before
 
     # Fourth case: when stoploss is set and it is hit
     # should unset stoploss_order_id and return true
     # as a trade actually happened
     caplog.clear()
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    stop_order_dict.update({'id': "104"})
+
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
-    trade.stoploss_order_id = "100"
+    trade.stoploss_order_id = "104"
     trade.orders.append(Order(
         ft_order_side='stoploss',
-        order_id='100',
+        order_id='104',
         ft_pair=trade.pair,
         ft_is_open=True,
         ft_amount=trade.amount,
@@ -1174,24 +1236,21 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
     assert trade
 
     stoploss_order_hit = MagicMock(return_value={
-        'id': "100",
+        'id': "104",
         'status': 'closed',
         'type': 'stop_loss_limit',
         'price': 3,
         'average': 2,
         'amount': enter_order['amount'],
     })
-    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit)
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
     assert freqtrade.handle_stoploss_on_exchange(trade) is True
     assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
     assert trade.stoploss_order_id is None
     assert trade.is_open is False
     caplog.clear()
 
-    mocker.patch(
-        'freqtrade.exchange.Exchange.stoploss',
-        side_effect=ExchangeError()
-    )
+    mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError())
     trade.is_open = True
     freqtrade.handle_stoploss_on_exchange(trade)
     assert log_has('Unable to place a stoploss order on exchange.', caplog)
@@ -1199,11 +1258,11 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
 
     # Fifth case: fetch_order returns InvalidOrder
     # It should try to add stoploss order
-    trade.stoploss_order_id = 100
+    stop_order_dict.update({'id': "105"})
+    trade.stoploss_order_id = "105"
     stoploss.reset_mock()
-    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
-                 side_effect=InvalidOrderException())
-    mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException())
+    mocker.patch(f'{EXMS}.create_stoploss', stoploss)
     freqtrade.handle_stoploss_on_exchange(trade)
     assert stoploss.call_count == 1
 
@@ -1212,39 +1271,185 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
     trade.stoploss_order_id = None
     trade.is_open = False
     stoploss.reset_mock()
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order')
-    mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
+    mocker.patch(f'{EXMS}.fetch_order')
+    mocker.patch(f'{EXMS}.create_stoploss', stoploss)
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
     assert stoploss.call_count == 0
 
     # Seventh case: emergency exit triggered
     # Trailing stop should not act anymore
     stoploss_order_cancelled = MagicMock(side_effect=[{
-        'id': "100",
+        'id': "107",
         'status': 'canceled',
         'type': 'stop_loss_limit',
         'price': 3,
         'average': 2,
         'amount': enter_order['amount'],
+        'filled': 0,
+        'remaining': enter_order['amount'],
         'info': {'stopPrice': 22},
     }])
-    trade.stoploss_order_id = 100
+    trade.stoploss_order_id = "107"
     trade.is_open = True
-    trade.stoploss_last_update = arrow.utcnow().shift(hours=-1).datetime
+    trade.stoploss_last_update = dt_now() - timedelta(hours=1)
     trade.stop_loss = 24
+    trade.exit_reason = None
+    trade.orders.append(
+        Order(
+            ft_order_side='stoploss',
+            ft_pair=trade.pair,
+            ft_is_open=True,
+            ft_amount=trade.amount,
+            ft_price=trade.stop_loss,
+            order_id='107',
+            status='open',
+        )
+    )
     freqtrade.config['trailing_stop'] = True
     stoploss = MagicMock(side_effect=InvalidOrderException())
 
-    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order_with_result',
+    Trade.commit()
+    mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result',
                  side_effect=InvalidOrderException())
-    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_cancelled)
-    mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled)
+    mocker.patch(f'{EXMS}.create_stoploss', stoploss)
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
     assert trade.stoploss_order_id is None
     assert trade.is_open is False
     assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT)
 
 
+@pytest.mark.parametrize("is_short", [False, True])
+def test_handle_stoploss_on_exchange_partial(
+        mocker, default_conf_usdt, fee, is_short, limit_order) -> None:
+    stop_order_dict = {'id': "101", "status": "open"}
+    stoploss = MagicMock(return_value=stop_order_dict)
+    enter_order = limit_order[entry_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
+    patch_RPCManager(mocker)
+    patch_exchange(mocker)
+    mocker.patch.multiple(
+        EXMS,
+        fetch_ticker=MagicMock(return_value={
+            'bid': 1.9,
+            'ask': 2.2,
+            'last': 1.9
+        }),
+        create_order=MagicMock(side_effect=[
+            enter_order,
+            exit_order,
+        ]),
+        get_fee=fee,
+        create_stoploss=stoploss
+    )
+    freqtrade = FreqtradeBot(default_conf_usdt)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
+
+    freqtrade.enter_positions()
+    trade = Trade.session.scalars(select(Trade)).first()
+    trade.is_short = is_short
+    trade.is_open = True
+    trade.open_order_id = None
+    trade.stoploss_order_id = None
+
+    assert freqtrade.handle_stoploss_on_exchange(trade) is False
+    assert stoploss.call_count == 1
+    assert trade.stoploss_order_id == "101"
+    assert trade.amount == 30
+    stop_order_dict.update({'id': "102"})
+    # Stoploss on exchange is cancelled on exchange, but filled partially.
+    # Must update trade amount to guarantee successful exit.
+    stoploss_order_hit = MagicMock(return_value={
+        'id': "101",
+        'status': 'canceled',
+        'type': 'stop_loss_limit',
+        'price': 3,
+        'average': 2,
+        'filled': trade.amount / 2,
+        'remaining': trade.amount / 2,
+        'amount': enter_order['amount'],
+    })
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
+    assert freqtrade.handle_stoploss_on_exchange(trade) is False
+    # Stoploss filled partially ...
+    assert trade.amount == 15
+
+    assert trade.stoploss_order_id == "102"
+
+
+@pytest.mark.parametrize("is_short", [False, True])
+def test_handle_stoploss_on_exchange_partial_cancel_here(
+        mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None:
+    stop_order_dict = {'id': "101", "status": "open"}
+    default_conf_usdt['trailing_stop'] = True
+    stoploss = MagicMock(return_value=stop_order_dict)
+    enter_order = limit_order[entry_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
+    patch_RPCManager(mocker)
+    patch_exchange(mocker)
+    mocker.patch.multiple(
+        EXMS,
+        fetch_ticker=MagicMock(return_value={
+            'bid': 1.9,
+            'ask': 2.2,
+            'last': 1.9
+        }),
+        create_order=MagicMock(side_effect=[
+            enter_order,
+            exit_order,
+        ]),
+        get_fee=fee,
+        create_stoploss=stoploss
+    )
+    freqtrade = FreqtradeBot(default_conf_usdt)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
+
+    freqtrade.enter_positions()
+    trade = Trade.session.scalars(select(Trade)).first()
+    trade.is_short = is_short
+    trade.is_open = True
+    trade.open_order_id = None
+    trade.stoploss_order_id = None
+
+    assert freqtrade.handle_stoploss_on_exchange(trade) is False
+    assert stoploss.call_count == 1
+    assert trade.stoploss_order_id == "101"
+    assert trade.amount == 30
+    stop_order_dict.update({'id': "102"})
+    # Stoploss on exchange is open.
+    # Freqtrade cancels the stop - but cancel returns a partial filled order.
+    stoploss_order_hit = MagicMock(return_value={
+        'id': "101",
+        'status': 'open',
+        'type': 'stop_loss_limit',
+        'price': 3,
+        'average': 2,
+        'filled': 0,
+        'remaining': trade.amount,
+        'amount': enter_order['amount'],
+    })
+    stoploss_order_cancel = MagicMock(return_value={
+        'id': "101",
+        'status': 'canceled',
+        'type': 'stop_loss_limit',
+        'price': 3,
+        'average': 2,
+        'filled': trade.amount / 2,
+        'remaining': trade.amount / 2,
+        'amount': enter_order['amount'],
+    })
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit)
+    mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel)
+    trade.stoploss_last_update = dt_now() - timedelta(minutes=10)
+
+    assert freqtrade.handle_stoploss_on_exchange(trade) is False
+    # Canceled Stoploss filled partially ...
+    assert log_has_re('Cancelling current stoploss on exchange.*', caplog)
+
+    assert trade.stoploss_order_id == "102"
+    assert trade.amount == 15
+
+
 @pytest.mark.parametrize("is_short", [False, True])
 def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
                                          limit_order) -> None:
@@ -1254,7 +1459,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -1267,19 +1472,30 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
         get_fee=fee,
     )
     mocker.patch.multiple(
-        'freqtrade.exchange.Binance',
+        EXMS,
         fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}),
-        stoploss=MagicMock(side_effect=ExchangeError()),
+        create_stoploss=MagicMock(side_effect=ExchangeError()),
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     freqtrade.enter_positions()
-    trade = Trade.query.first()
-    trade.is_short = is_short
+    trade = Trade.session.scalars(select(Trade)).first()
+    assert trade.is_short == is_short
     trade.is_open = True
     trade.open_order_id = None
-    trade.stoploss_order_id = 100
+    trade.stoploss_order_id = "100"
+    trade.orders.append(
+        Order(
+            ft_order_side='stoploss',
+            ft_pair=trade.pair,
+            ft_is_open=True,
+            ft_amount=trade.amount,
+            ft_price=trade.stop_loss,
+            order_id='100',
+            status='open',
+        )
+    )
     assert trade
 
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
@@ -1301,7 +1517,7 @@ def test_create_stoploss_order_invalid_order(
         {'id': order['id']}
     ])
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -1311,16 +1527,16 @@ def test_create_stoploss_order_invalid_order(
         get_fee=fee,
     )
     mocker.patch.multiple(
-        'freqtrade.exchange.Binance',
+        EXMS,
         fetch_order=MagicMock(return_value={'status': 'canceled'}),
-        stoploss=MagicMock(side_effect=InvalidOrderException()),
+        create_stoploss=MagicMock(side_effect=InvalidOrderException()),
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
 
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     caplog.clear()
     freqtrade.create_stoploss_order(trade, 200)
@@ -1350,7 +1566,7 @@ def test_create_stoploss_order_insufficient_funds(
 
     mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds')
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -1364,14 +1580,14 @@ def test_create_stoploss_order_insufficient_funds(
         fetch_order=MagicMock(return_value={'status': 'canceled'}),
     )
     mocker.patch.multiple(
-        'freqtrade.exchange.Binance',
-        stoploss=MagicMock(side_effect=InsufficientFundsError()),
+        EXMS,
+        create_stoploss=MagicMock(side_effect=InsufficientFundsError()),
     )
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
 
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     caplog.clear()
     freqtrade.create_stoploss_order(trade, 200)
@@ -1398,10 +1614,10 @@ def test_handle_stoploss_on_exchange_trailing(
     # When trailing stoploss is set
     enter_order = limit_order[entry_side(is_short)]
     exit_order = limit_order[exit_side(is_short)]
-    stoploss = MagicMock(return_value={'id': 13434334})
+    stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'})
     patch_RPCManager(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 2.19,
             'ask': 2.2,
@@ -1414,8 +1630,8 @@ def test_handle_stoploss_on_exchange_trailing(
         get_fee=fee,
     )
     mocker.patch.multiple(
-        'freqtrade.exchange.Binance',
-        stoploss=stoploss,
+        EXMS,
+        create_stoploss=stoploss,
         stoploss_adjust=MagicMock(return_value=True),
     )
 
@@ -1439,15 +1655,25 @@ def test_handle_stoploss_on_exchange_trailing(
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
-    trade.stoploss_order_id = 100
-    trade.stoploss_last_update = arrow.utcnow().shift(minutes=-20).datetime
+    trade.stoploss_order_id = '100'
+    trade.stoploss_last_update = dt_now() - timedelta(minutes=20)
+    trade.orders.append(
+        Order(
+            ft_order_side='stoploss',
+            ft_pair=trade.pair,
+            ft_is_open=True,
+            ft_amount=trade.amount,
+            ft_price=trade.stop_loss,
+            order_id='100',
+        )
+    )
 
     stoploss_order_hanging = MagicMock(return_value={
-        'id': 100,
+        'id': '100',
         'status': 'open',
         'type': 'stop_loss_limit',
         'price': hang_price,
@@ -1457,7 +1683,7 @@ def test_handle_stoploss_on_exchange_trailing(
         }
     })
 
-    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging)
 
     # stoploss initially at 5%
     assert freqtrade.handle_trade(trade) is False
@@ -1465,7 +1691,7 @@ def test_handle_stoploss_on_exchange_trailing(
 
     # price jumped 2x
     mocker.patch(
-        'freqtrade.exchange.Exchange.fetch_ticker',
+        f'{EXMS}.fetch_ticker',
         MagicMock(return_value={
             'bid': bid[0],
             'ask': ask[0],
@@ -1474,9 +1700,9 @@ def test_handle_stoploss_on_exchange_trailing(
     )
 
     cancel_order_mock = MagicMock()
-    stoploss_order_mock = MagicMock(return_value={'id': 'so1'})
-    mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock)
-    mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
+    stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
+    mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
+    mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
 
     # stoploss should not be updated as the interval is 60 seconds
     assert freqtrade.handle_trade(trade) is False
@@ -1486,13 +1712,14 @@ def test_handle_stoploss_on_exchange_trailing(
 
     assert freqtrade.handle_trade(trade) is False
     assert trade.stop_loss == stop_price[1]
+    trade.stoploss_order_id = '100'
 
     # setting stoploss_on_exchange_interval to 0 seconds
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
 
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
 
-    cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
+    cancel_order_mock.assert_called_once_with('100', 'ETH/USDT')
     stoploss_order_mock.assert_called_once_with(
         amount=pytest.approx(amt),
         pair='ETH/USDT',
@@ -1504,7 +1731,7 @@ def test_handle_stoploss_on_exchange_trailing(
 
     # price fell below stoploss, so dry-run sells trade.
     mocker.patch(
-        'freqtrade.exchange.Exchange.fetch_ticker',
+        f'{EXMS}.fetch_ticker',
         MagicMock(return_value={
             'bid': bid[1],
             'ask': ask[1],
@@ -1522,11 +1749,11 @@ def test_handle_stoploss_on_exchange_trailing_error(
     enter_order = limit_order[entry_side(is_short)]
     exit_order = limit_order[exit_side(is_short)]
     # When trailing stoploss is set
-    stoploss = MagicMock(return_value={'id': 13434334})
+    stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
     patch_exchange(mocker)
 
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -1539,8 +1766,8 @@ def test_handle_stoploss_on_exchange_trailing_error(
         get_fee=fee,
     )
     mocker.patch.multiple(
-        'freqtrade.exchange.Binance',
-        stoploss=stoploss,
+        EXMS,
+        create_stoploss=stoploss,
         stoploss_adjust=MagicMock(return_value=True),
     )
 
@@ -1558,13 +1785,13 @@ def test_handle_stoploss_on_exchange_trailing_error(
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
     trade.stoploss_order_id = "abcd"
     trade.stop_loss = 0.2
-    trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None)
+    trade.stoploss_last_update = (dt_now() - timedelta(minutes=601)).replace(tzinfo=None)
     trade.is_short = is_short
 
     stoploss_order_hanging = {
@@ -1577,9 +1804,9 @@ def test_handle_stoploss_on_exchange_trailing_error(
             'stopPrice': '0.1'
         }
     }
-    mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order',
+    mocker.patch(f'{EXMS}.cancel_stoploss_order',
                  side_effect=InvalidOrderException())
-    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
+    mocker.patch(f'{EXMS}.fetch_stoploss_order',
                  return_value=stoploss_order_hanging)
     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog)
@@ -1588,10 +1815,10 @@ def test_handle_stoploss_on_exchange_trailing_error(
     assert stoploss.call_count == 1
 
     # Fail creating stoploss order
-    trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime
+    trade.stoploss_last_update = dt_now() - timedelta(minutes=601)
     caplog.clear()
-    cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock())
-    mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError())
+    cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order')
+    mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError())
     freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
     assert cancel_mock.call_count == 1
     assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog)
@@ -1601,15 +1828,15 @@ def test_stoploss_on_exchange_price_rounding(
         mocker, default_conf_usdt, fee, open_trade_usdt) -> None:
     patch_RPCManager(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_fee=fee,
     )
-    price_mock = MagicMock(side_effect=lambda p, s: int(s))
+    price_mock = MagicMock(side_effect=lambda p, s, **kwargs: int(s))
     stoploss_mock = MagicMock(return_value={'id': '13434334'})
     adjust_mock = MagicMock(return_value=False)
     mocker.patch.multiple(
-        'freqtrade.exchange.Binance',
-        stoploss=stoploss_mock,
+        EXMS,
+        create_stoploss=stoploss_mock,
         stoploss_adjust=adjust_mock,
         price_to_precision=price_mock,
     )
@@ -1631,10 +1858,10 @@ def test_handle_stoploss_on_exchange_custom_stop(
     enter_order = limit_order[entry_side(is_short)]
     exit_order = limit_order[exit_side(is_short)]
     # When trailing stoploss is set
-    stoploss = MagicMock(return_value={'id': 13434334})
+    stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'})
     patch_RPCManager(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -1647,8 +1874,8 @@ def test_handle_stoploss_on_exchange_custom_stop(
         get_fee=fee,
     )
     mocker.patch.multiple(
-        'freqtrade.exchange.Binance',
-        stoploss=stoploss,
+        EXMS,
+        create_stoploss=stoploss,
         stoploss_adjust=MagicMock(return_value=True),
     )
 
@@ -1672,15 +1899,25 @@ def test_handle_stoploss_on_exchange_custom_stop(
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
-    trade.stoploss_order_id = 100
-    trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime
+    trade.stoploss_order_id = '100'
+    trade.stoploss_last_update = dt_now() - timedelta(minutes=601)
+    trade.orders.append(
+        Order(
+            ft_order_side='stoploss',
+            ft_pair=trade.pair,
+            ft_is_open=True,
+            ft_amount=trade.amount,
+            ft_price=trade.stop_loss,
+            order_id='100',
+        )
+    )
 
     stoploss_order_hanging = MagicMock(return_value={
-        'id': 100,
+        'id': '100',
         'status': 'open',
         'type': 'stop_loss_limit',
         'price': 3,
@@ -1690,14 +1927,14 @@ def test_handle_stoploss_on_exchange_custom_stop(
         }
     })
 
-    mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging)
 
     assert freqtrade.handle_trade(trade) is False
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
 
     # price jumped 2x
     mocker.patch(
-        'freqtrade.exchange.Exchange.fetch_ticker',
+        f'{EXMS}.fetch_ticker',
         MagicMock(return_value={
             'bid': 4.38 if not is_short else 1.9 / 2,
             'ask': 4.4 if not is_short else 2.2 / 2,
@@ -1706,9 +1943,10 @@ def test_handle_stoploss_on_exchange_custom_stop(
     )
 
     cancel_order_mock = MagicMock()
-    stoploss_order_mock = MagicMock(return_value={'id': 'so1'})
-    mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock)
-    mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
+    stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'})
+    mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
+    mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
+    trade.stoploss_order_id = '100'
 
     # stoploss should not be updated as the interval is 60 seconds
     assert freqtrade.handle_trade(trade) is False
@@ -1725,7 +1963,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
 
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
 
-    cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
+    cancel_order_mock.assert_called_once_with('100', 'ETH/USDT')
     # Long uses modified ask - offset, short modified bid + offset
     stoploss_order_mock.assert_called_once_with(
         amount=pytest.approx(trade.amount),
@@ -1738,7 +1976,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
 
     # price fell below stoploss, so dry-run sells trade.
     mocker.patch(
-        'freqtrade.exchange.Exchange.fetch_ticker',
+        f'{EXMS}.fetch_ticker',
         MagicMock(return_value={
             'bid': 4.17,
             'ask': 4.19,
@@ -1754,7 +1992,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
     exit_order = limit_order['sell']
 
     # When trailing stoploss is set
-    stoploss = MagicMock(return_value={'id': 13434334})
+    stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'})
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     patch_edge(mocker)
@@ -1762,7 +2000,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
     edge_conf['dry_run_wallet'] = 999.9
     edge_conf['exchange']['name'] = 'binance'
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 2.19,
             'ask': 2.2,
@@ -1773,7 +2011,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
             {'id': exit_order['id']},
         ]),
         get_fee=fee,
-        stoploss=stoploss,
+        create_stoploss=stoploss,
     )
 
     # enabling TSL
@@ -1800,14 +2038,24 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
     freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist)
 
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_open = True
     trade.open_order_id = None
-    trade.stoploss_order_id = 100
-    trade.stoploss_last_update = arrow.utcnow()
+    trade.stoploss_order_id = '100'
+    trade.stoploss_last_update = dt_now()
+    trade.orders.append(
+        Order(
+            ft_order_side='stoploss',
+            ft_pair=trade.pair,
+            ft_is_open=True,
+            ft_amount=trade.amount,
+            ft_price=trade.stop_loss,
+            order_id='100',
+        )
+    )
 
     stoploss_order_hanging = MagicMock(return_value={
-        'id': 100,
+        'id': '100',
         'status': 'open',
         'type': 'stop_loss_limit',
         'price': 3,
@@ -1815,7 +2063,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
         'stopPrice': '2.178'
     })
 
-    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging)
 
     # stoploss initially at 20% as edge dictated it.
     assert freqtrade.handle_trade(trade) is False
@@ -1824,11 +2072,11 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
 
     cancel_order_mock = MagicMock()
     stoploss_order_mock = MagicMock()
-    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
-    mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
+    mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock)
+    mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock)
 
     # price goes down 5%
-    mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
+    mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={
         'bid': 2.19 * 0.95,
         'ask': 2.2 * 0.95,
         'last': 2.19 * 0.95
@@ -1843,7 +2091,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
     cancel_order_mock.assert_not_called()
 
     # price jumped 2x
-    mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
+    mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={
         'bid': 4.38,
         'ask': 4.4,
         'last': 4.38
@@ -1854,7 +2102,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde
 
     # stoploss should be set to 1% as trailing is on
     assert trade.stop_loss == 4.4 * 0.99
-    cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
+    cancel_order_mock.assert_called_once_with('100', 'NEO/BTC')
     stoploss_order_mock.assert_called_once_with(
         amount=pytest.approx(11.41438356),
         pair='NEO/BTC',
@@ -1888,21 +2136,42 @@ def test_enter_positions(mocker, default_conf_usdt, return_value, side_effect,
     assert mock_ct.call_count == len(default_conf_usdt['exchange']['pair_whitelist'])
 
 
+@pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize("is_short", [False, True])
 def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
 
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
-                 return_value=limit_order[entry_side(is_short)])
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
+    mocker.patch(f'{EXMS}.fetch_order', return_value=limit_order[entry_side(is_short)])
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
 
-    # TODO: should not be magicmock
-    trade = MagicMock()
-    trade.is_short = is_short
-    trade.open_order_id = '123'
-    trade.open_fee = 0.001
+    order_id = '123'
+    trade = Trade(
+            open_order_id=order_id,
+            pair='ETH/USDT',
+            fee_open=0.001,
+            fee_close=0.001,
+            open_rate=0.01,
+            open_date=dt_now(),
+            stake_amount=0.01,
+            amount=11,
+            exchange="binance",
+            is_short=is_short,
+            leverage=1,
+            )
+    trade.orders.append(Order(
+        ft_order_side=entry_side(is_short),
+        price=0.01,
+        ft_pair=trade.pair,
+        ft_amount=trade.amount,
+        ft_price=trade.open_rate,
+        order_id=order_id,
+
+    ))
+    Trade.session.add(trade)
+    Trade.commit()
     trades = [trade]
+    freqtrade.wallets.update()
     n = freqtrade.exit_positions(trades)
     assert n == 0
     # Test amount not modified by fee-logic
@@ -1915,17 +2184,40 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog
     assert gra.call_count == 0
 
 
+@pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize("is_short", [False, True])
 def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog, is_short) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     order = limit_order[entry_side(is_short)]
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
+    mocker.patch(f'{EXMS}.fetch_order', return_value=order)
 
-    # TODO: should not be magicmock
-    trade = MagicMock()
-    trade.is_short = is_short
+    order_id = '123'
+    trade = Trade(
+        open_order_id=order_id,
+        pair='ETH/USDT',
+        fee_open=0.001,
+        fee_close=0.001,
+        open_rate=0.01,
+        open_date=dt_now(),
+        stake_amount=0.01,
+        amount=11,
+        exchange="binance",
+        is_short=is_short,
+        leverage=1,
+    )
+    trade.orders.append(Order(
+        ft_order_side=entry_side(is_short),
+        price=0.01,
+        ft_pair=trade.pair,
+        ft_amount=trade.amount,
+        ft_price=trade.open_rate,
+        order_id=order_id,
+
+    ))
     trade.open_order_id = None
-    trade.pair = 'ETH/USDT'
+    Trade.session.add(trade)
+    Trade.commit()
+    freqtrade.wallets.update()
     trades = [trade]
 
     # Test raise of DependencyException exception
@@ -1945,8 +2237,8 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
     order = limit_order[entry_side(is_short)]
 
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
+    mocker.patch(f'{EXMS}.fetch_order', return_value=order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.0)
     order_id = order['id']
 
@@ -1955,7 +2247,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
         fee_open=0.001,
         fee_close=0.001,
         open_rate=0.01,
-        open_date=arrow.utcnow().datetime,
+        open_date=dt_now(),
         amount=11,
         exchange="binance",
         is_short=is_short,
@@ -1997,7 +2289,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca
     limit_buy_order_usdt_new['status'] = 'canceled'
 
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError)
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt_new)
+    mocker.patch(f'{EXMS}.fetch_order', return_value=limit_buy_order_usdt_new)
     res = freqtrade.update_trade_state(trade, order_id)
     # Cancelled empty
     assert res is True
@@ -2016,9 +2308,9 @@ def test_update_trade_state_withorderdict(
     trades_for_order[0]['amount'] = initial_amount
     order_id = "oid_123456"
     order['id'] = order_id
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order)
     # fetch_order should not be called!!
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
+    mocker.patch(f'{EXMS}.fetch_order', MagicMock(side_effect=ValueError))
     patch_exchange(mocker)
     amount = sum(x['amount'] for x in trades_for_order)
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
@@ -2028,7 +2320,7 @@ def test_update_trade_state_withorderdict(
         amount=amount,
         exchange='binance',
         open_rate=2.0,
-        open_date=arrow.utcnow().datetime,
+        open_date=dt_now(),
         fee_open=fee.return_value,
         fee_close=fee.return_value,
         open_order_id=order_id,
@@ -2060,7 +2352,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit
                                       caplog) -> None:
     order = limit_order[entry_side(is_short)]
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
+    mocker.patch(f'{EXMS}.fetch_order', return_value=order)
 
     # TODO: should not be magicmock
     trade = MagicMock()
@@ -2078,8 +2370,7 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit
 
 def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
-                 MagicMock(side_effect=InvalidOrderException))
+    mocker.patch(f'{EXMS}.fetch_order', MagicMock(side_effect=InvalidOrderException))
 
     # TODO: should not be magicmock
     trade = MagicMock()
@@ -2099,9 +2390,9 @@ def test_update_trade_state_sell(
     buy_order = limit_order[entry_side(is_short)]
     open_order = limit_order_open[exit_side(is_short)]
     l_order = limit_order[exit_side(is_short)]
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order)
     # fetch_order should not be called!!
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
+    mocker.patch(f'{EXMS}.fetch_order', MagicMock(side_effect=ValueError))
     wallet_mock = MagicMock()
     mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock)
 
@@ -2116,7 +2407,7 @@ def test_update_trade_state_sell(
         open_rate=0.245441,
         fee_open=0.0025,
         fee_close=0.0025,
-        open_date=arrow.utcnow().datetime,
+        open_date=dt_now(),
         open_order_id=open_order['id'],
         is_open=True,
         interest_rate=0.0005,
@@ -2151,7 +2442,7 @@ def test_handle_trade(
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 2.19,
             'ask': 2.2,
@@ -2168,7 +2459,7 @@ def test_handle_trade(
 
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
 
@@ -2204,7 +2495,7 @@ def test_handle_overlapping_signals(
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(side_effect=[
             open_order,
@@ -2223,7 +2514,7 @@ def test_handle_overlapping_signals(
     freqtrade.enter_positions()
 
     # Buy and Sell triggering, so doing nothing ...
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
 
     nb_trades = len(trades)
     assert nb_trades == 0
@@ -2231,7 +2522,7 @@ def test_handle_overlapping_signals(
     # Buy is triggering, so buying ...
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.enter_positions()
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
     for trade in trades:
         trade.is_short = is_short
     nb_trades = len(trades)
@@ -2241,7 +2532,7 @@ def test_handle_overlapping_signals(
     # Buy and Sell are not triggering, so doing nothing ...
     patch_get_signal(freqtrade, enter_long=False)
     assert freqtrade.handle_trade(trades[0]) is False
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
     for trade in trades:
         trade.is_short = is_short
     nb_trades = len(trades)
@@ -2254,7 +2545,7 @@ def test_handle_overlapping_signals(
     else:
         patch_get_signal(freqtrade, enter_long=True, exit_long=True)
     assert freqtrade.handle_trade(trades[0]) is False
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
     for trade in trades:
         trade.is_short = is_short
     nb_trades = len(trades)
@@ -2266,7 +2557,7 @@ def test_handle_overlapping_signals(
         patch_get_signal(freqtrade, enter_long=False, exit_short=True)
     else:
         patch_get_signal(freqtrade, enter_long=False, exit_long=True)
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
     for trade in trades:
         trade.is_short = is_short
     assert freqtrade.handle_trade(trades[0]) is True
@@ -2282,7 +2573,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee,
 
     patch_RPCManager(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(side_effect=[
             open_order,
@@ -2297,7 +2588,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee,
 
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.is_open = True
 
@@ -2325,7 +2616,7 @@ def test_handle_trade_use_exit_signal(
     caplog.set_level(logging.DEBUG)
     patch_RPCManager(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(side_effect=[
             enter_open_order,
@@ -2339,7 +2630,7 @@ def test_handle_trade_use_exit_signal(
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.is_open = True
 
@@ -2365,7 +2656,7 @@ def test_close_trade(
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=open_order),
         get_fee=fee,
@@ -2376,7 +2667,7 @@ def test_close_trade(
     # Create trade and sell it
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
 
@@ -2422,7 +2713,7 @@ def test_manage_open_orders_entry_usercustom(
 
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=old_order),
         cancel_order=cancel_order_mock,
@@ -2433,7 +2724,7 @@ def test_manage_open_orders_entry_usercustom(
     open_trade.is_short = is_short
     open_trade.orders[0].side = 'sell' if is_short else 'buy'
     open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy'
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
 
     # Ensure default is to return empty (so not mocked yet)
@@ -2444,7 +2735,8 @@ def test_manage_open_orders_entry_usercustom(
     freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
     freqtrade.manage_open_orders()
     assert cancel_order_mock.call_count == 0
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     nb_trades = len(trades)
     assert nb_trades == 1
     assert freqtrade.strategy.check_entry_timeout.call_count == 1
@@ -2452,7 +2744,8 @@ def test_manage_open_orders_entry_usercustom(
 
     freqtrade.manage_open_orders()
     assert cancel_order_mock.call_count == 0
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     nb_trades = len(trades)
     assert nb_trades == 1
     assert freqtrade.strategy.check_entry_timeout.call_count == 1
@@ -2462,7 +2755,8 @@ def test_manage_open_orders_entry_usercustom(
     freqtrade.manage_open_orders()
     assert cancel_order_wr_mock.call_count == 1
     assert rpc_mock.call_count == 2
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     nb_trades = len(trades)
     assert nb_trades == 0
     assert freqtrade.strategy.check_entry_timeout.call_count == 1
@@ -2483,7 +2777,7 @@ def test_manage_open_orders_entry(
     cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=old_order),
         cancel_order_with_result=cancel_order_mock,
@@ -2492,7 +2786,7 @@ def test_manage_open_orders_entry(
     freqtrade = FreqtradeBot(default_conf_usdt)
 
     open_trade.is_short = is_short
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
 
     freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False)
@@ -2501,7 +2795,8 @@ def test_manage_open_orders_entry(
     freqtrade.manage_open_orders()
     assert cancel_order_mock.call_count == 1
     assert rpc_mock.call_count == 2
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     nb_trades = len(trades)
     assert nb_trades == 0
     # Custom user buy-timeout is never called
@@ -2522,7 +2817,7 @@ def test_adjust_entry_cancel(
     limit_buy_cancel['status'] = 'canceled'
     cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=old_order),
         cancel_order_with_result=cancel_order_mock,
@@ -2530,7 +2825,7 @@ def test_adjust_entry_cancel(
     )
 
     open_trade.is_short = is_short
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
 
     # Timeout to not interfere
@@ -2539,9 +2834,10 @@ def test_adjust_entry_cancel(
     # check that order is cancelled
     freqtrade.strategy.adjust_entry_price = MagicMock(return_value=None)
     freqtrade.manage_open_orders()
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     assert len(trades) == 0
-    assert len(Order.query.all()) == 0
+    assert len(Order.session.scalars(select(Order)).all()) == 0
     assert log_has_re(
         f"{'Sell' if is_short else 'Buy'} order user requested order cancel*", caplog)
     assert log_has_re(
@@ -2563,7 +2859,7 @@ def test_adjust_entry_maintain_replace(
     limit_buy_cancel['status'] = 'canceled'
     cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=old_order),
         cancel_order_with_result=cancel_order_mock,
@@ -2571,7 +2867,7 @@ def test_adjust_entry_maintain_replace(
     )
 
     open_trade.is_short = is_short
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
 
     # Timeout to not interfere
@@ -2580,7 +2876,8 @@ def test_adjust_entry_maintain_replace(
     # Check that order is maintained
     freqtrade.strategy.adjust_entry_price = MagicMock(return_value=old_order['price'])
     freqtrade.manage_open_orders()
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     assert len(trades) == 1
     assert len(Order.get_open_orders()) == 1
     # Entry adjustment is called
@@ -2590,9 +2887,10 @@ def test_adjust_entry_maintain_replace(
     freqtrade.get_valid_enter_price_and_stake = MagicMock(return_value={100, 10, 1})
     freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234)
     freqtrade.manage_open_orders()
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     assert len(trades) == 1
-    nb_all_orders = len(Order.query.all())
+    nb_all_orders = len(Order.session.scalars(select(Order)).all())
     assert nb_all_orders == 2
     # New order seems to be in closed status?
     # nb_open_orders = len(Order.get_open_orders())
@@ -2615,7 +2913,7 @@ def test_check_handle_cancelled_buy(
     patch_exchange(mocker)
     old_order.update({"status": "canceled", 'filled': 0.0})
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=old_order),
         cancel_order=cancel_order_mock,
@@ -2624,14 +2922,15 @@ def test_check_handle_cancelled_buy(
     freqtrade = FreqtradeBot(default_conf_usdt)
     open_trade.orders = []
     open_trade.is_short = is_short
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
 
     # check it does cancel buy orders over the time limit
     freqtrade.manage_open_orders()
     assert cancel_order_mock.call_count == 0
     assert rpc_mock.call_count == 2
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     assert len(trades) == 0
     assert log_has_re(
         f"{'Sell' if is_short else 'Buy'} order cancelled on exchange for Trade.*", caplog)
@@ -2645,7 +2944,7 @@ def test_manage_open_orders_buy_exception(
     cancel_order_mock = MagicMock()
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         validate_pairs=MagicMock(),
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(side_effect=ExchangeError),
@@ -2655,14 +2954,15 @@ def test_manage_open_orders_buy_exception(
     freqtrade = FreqtradeBot(default_conf_usdt)
 
     open_trade.is_short = is_short
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
 
     # check it does cancel buy orders over the time limit
     freqtrade.manage_open_orders()
     assert cancel_order_mock.call_count == 0
     assert rpc_mock.call_count == 1
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     nb_trades = len(trades)
     assert nb_trades == 1
 
@@ -2683,21 +2983,21 @@ def test_manage_open_orders_exit_usercustom(
     rpc_mock = patch_RPCManager(mocker)
     cancel_order_mock = MagicMock()
     patch_exchange(mocker)
-    mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.0)
+    mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.0)
     et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=limit_sell_order_old),
         cancel_order=cancel_order_mock
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
 
-    open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime
-    open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime
+    open_trade_usdt.open_date = dt_now() - timedelta(hours=5)
+    open_trade_usdt.close_date = dt_now() - timedelta(minutes=601)
     open_trade_usdt.close_profit_abs = 0.001
 
-    Trade.query.session.add(open_trade_usdt)
+    Trade.session.add(open_trade_usdt)
     Trade.commit()
     # Ensure default is false
     freqtrade.manage_open_orders()
@@ -2729,22 +3029,25 @@ def test_manage_open_orders_exit_usercustom(
     assert rpc_mock.call_count == 2
     assert freqtrade.strategy.check_exit_timeout.call_count == 1
     assert freqtrade.strategy.check_entry_timeout.call_count == 0
+    trade = Trade.session.scalars(select(Trade)).first()
+    # cancelling didn't succeed - order-id remains open.
+    assert trade.open_order_id is not None
 
-    # 2nd canceled trade - Fail execute sell
+    # 2nd canceled trade - Fail execute exit
     caplog.clear()
     open_trade_usdt.open_order_id = limit_sell_order_old['id']
     mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
                  side_effect=DependencyException)
     freqtrade.manage_open_orders()
-    assert log_has_re('Unable to emergency sell .*', caplog)
+    assert log_has_re('Unable to emergency exit .*', caplog)
 
     et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
     caplog.clear()
     # 2nd canceled trade ...
     open_trade_usdt.open_order_id = limit_sell_order_old['id']
 
-    # If cancelling fails - no emergency sell!
+    # If cancelling fails - no emergency exit!
     with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
         freqtrade.manage_open_orders()
         assert et_mock.call_count == 0
@@ -2764,7 +3067,7 @@ def test_manage_open_orders_exit(
     limit_sell_order_old['side'] = 'buy' if is_short else 'sell'
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=limit_sell_order_old),
         cancel_order=cancel_order_mock,
@@ -2772,12 +3075,12 @@ def test_manage_open_orders_exit(
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
 
-    open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime
-    open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime
+    open_trade_usdt.open_date = dt_now() - timedelta(hours=5)
+    open_trade_usdt.close_date = dt_now() - timedelta(minutes=601)
     open_trade_usdt.close_profit_abs = 0.001
     open_trade_usdt.is_short = is_short
 
-    Trade.query.session.add(open_trade_usdt)
+    Trade.session.add(open_trade_usdt)
     Trade.commit()
 
     freqtrade.strategy.check_exit_timeout = MagicMock(return_value=False)
@@ -2806,18 +3109,18 @@ def test_check_handle_cancelled_exit(
 
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=limit_sell_order_old),
         cancel_order_with_result=cancel_order_mock
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
 
-    open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime
-    open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime
+    open_trade_usdt.open_date = dt_now() - timedelta(hours=5)
+    open_trade_usdt.close_date = dt_now() - timedelta(minutes=601)
     open_trade_usdt.is_short = is_short
 
-    Trade.query.session.add(open_trade_usdt)
+    Trade.session.add(open_trade_usdt)
     Trade.commit()
 
     # check it does cancel sell orders over the time limit
@@ -2847,14 +3150,14 @@ def test_manage_open_orders_partial(
     cancel_order_mock = MagicMock(return_value=limit_buy_canceled)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=limit_buy_order_old_partial),
         cancel_order_with_result=cancel_order_mock
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
     prior_stake = open_trade.stake_amount
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
 
     # check it does cancel buy orders over the time limit
@@ -2862,7 +3165,8 @@ def test_manage_open_orders_partial(
     freqtrade.manage_open_orders()
     assert cancel_order_mock.call_count == 1
     assert rpc_mock.call_count == 3
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     assert len(trades) == 1
     assert trades[0].amount == 23.0
     assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount / leverage
@@ -2887,7 +3191,7 @@ def test_manage_open_orders_partial_fee(
     mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0))
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=limit_buy_order_old_partial),
         cancel_order_with_result=cancel_order_mock,
@@ -2899,7 +3203,7 @@ def test_manage_open_orders_partial_fee(
 
     open_trade.fee_open = fee()
     open_trade.fee_close = fee()
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
     # cancelling a half-filled order should update the amount to the bought amount
     # and apply fees if necessary.
@@ -2909,7 +3213,8 @@ def test_manage_open_orders_partial_fee(
 
     assert cancel_order_mock.call_count == 1
     assert rpc_mock.call_count == 3
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     assert len(trades) == 1
     # Verify that trade has been updated
     assert trades[0].amount == (limit_buy_order_old_partial['amount'] -
@@ -2935,7 +3240,7 @@ def test_manage_open_orders_partial_except(
     cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(return_value=limit_buy_order_old_partial),
         cancel_order_with_result=cancel_order_mock,
@@ -2949,7 +3254,7 @@ def test_manage_open_orders_partial_except(
 
     open_trade.fee_open = fee()
     open_trade.fee_close = fee()
-    Trade.query.session.add(open_trade)
+    Trade.session.add(open_trade)
     Trade.commit()
     # cancelling a half-filled order should update the amount to the bought amount
     # and apply fees if necessary.
@@ -2959,7 +3264,8 @@ def test_manage_open_orders_partial_except(
 
     assert cancel_order_mock.call_count == 1
     assert rpc_mock.call_count == 3
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    trades = Trade.session.scalars(
+        select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all()
     assert len(trades) == 1
     # Verify that trade has been updated
 
@@ -2981,14 +3287,14 @@ def test_manage_open_orders_exception(default_conf_usdt, ticker_usdt, open_trade
         handle_cancel_exit=MagicMock(),
     )
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         fetch_order=MagicMock(side_effect=ExchangeError('Oh snap')),
         cancel_order=cancel_order_mock
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
 
-    Trade.query.session.add(open_trade_usdt)
+    Trade.session.add(open_trade_usdt)
     Trade.commit()
 
     caplog.clear()
@@ -3011,13 +3317,13 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
     del cancel_buy_order['filled']
 
     cancel_order_mock = MagicMock(return_value=cancel_buy_order)
-    mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
+    mocker.patch(f'{EXMS}.cancel_order_with_result', cancel_order_mock)
 
     freqtrade = FreqtradeBot(default_conf_usdt)
     freqtrade._notify_enter_cancel = MagicMock()
 
     trade = mock_trade_usdt_4(fee, is_short)
-    Trade.query.session.add(trade)
+    Trade.session.add(trade)
     Trade.commit()
 
     l_order['filled'] = 0.0
@@ -3042,11 +3348,12 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_
     # Order remained open for some reason (cancel failed)
     cancel_buy_order['status'] = 'open'
     cancel_order_mock = MagicMock(return_value=cancel_buy_order)
-    mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
+    trade.open_order_id = 'some_open_order'
+    mocker.patch(f'{EXMS}.cancel_order_with_result', cancel_order_mock)
     assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert log_has_re(r"Order .* for .* not cancelled.", caplog)
     # min_pair_stake empty should not crash
-    mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None)
+    mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=None)
     assert not freqtrade.handle_cancel_enter(trade, limit_order[entry_side(is_short)], reason)
 
 
@@ -3058,15 +3365,15 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     cancel_order_mock = mocker.patch(
-        'freqtrade.exchange.Exchange.cancel_order_with_result',
+        f'{EXMS}.cancel_order_with_result',
         return_value=limit_buy_order_canceled_empty)
-    nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_enter_cancel')
+    notify_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_enter_cancel')
     freqtrade = FreqtradeBot(default_conf_usdt)
 
     reason = CANCEL_REASON['TIMEOUT']
 
     trade = mock_trade_usdt_4(fee, is_short)
-    Trade.query.session.add(trade)
+    Trade.session.add(trade)
     Trade.commit()
     assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
     assert cancel_order_mock.call_count == 0
@@ -3075,7 +3382,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
         r'Removing .* from database\.',
         caplog
     )
-    assert nofiy_mock.call_count == 1
+    assert notify_mock.call_count == 1
 
 
 @pytest.mark.parametrize("is_short", [False, True])
@@ -3092,7 +3399,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
     l_order = limit_order[entry_side(is_short)]
     cancel_order_mock = MagicMock(return_value=cancelorder)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         cancel_order=cancel_order_mock,
         fetch_order=MagicMock(side_effect=InvalidOrderException)
     )
@@ -3100,7 +3407,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
     freqtrade = FreqtradeBot(default_conf_usdt)
     freqtrade._notify_enter_cancel = MagicMock()
     trade = mock_trade_usdt_4(fee, is_short)
-    Trade.query.session.add(trade)
+    Trade.session.add(trade)
     Trade.commit()
     l_order['filled'] = 0.0
     l_order['status'] = 'open'
@@ -3112,7 +3419,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
     l_order['filled'] = 1.0
     order = deepcopy(l_order)
     order['status'] = 'canceled'
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
+    mocker.patch(f'{EXMS}.fetch_order', return_value=order)
     assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert cancel_order_mock.call_count == 1
 
@@ -3122,11 +3429,11 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
     patch_exchange(mocker)
     cancel_order_mock = MagicMock()
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         cancel_order=cancel_order_mock,
     )
-    mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.245441)
-    mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.2)
+    mocker.patch(f'{EXMS}.get_rate', return_value=0.245441)
+    mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.2)
 
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
 
@@ -3138,11 +3445,11 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
         exchange='binance',
         open_rate=0.245441,
         open_order_id="sell_123456",
-        open_date=arrow.utcnow().shift(days=-2).datetime,
+        open_date=dt_now() - timedelta(days=2),
         fee_open=fee.return_value,
         fee_close=fee.return_value,
         close_rate=0.555,
-        close_date=arrow.utcnow().datetime,
+        close_date=dt_now(),
         exit_reason="sell_reason_whatever",
         stake_amount=0.245441 * 2,
     )
@@ -3228,20 +3535,25 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
 def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.0)
-    mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result',
-                 side_effect=InvalidOrderException())
+    mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=0.0)
+    mocker.patch(f'{EXMS}.cancel_order_with_result', side_effect=InvalidOrderException())
 
     freqtrade = FreqtradeBot(default_conf_usdt)
 
     # TODO: should not be magicmock
     trade = MagicMock()
+    trade.open_order_id = '125'
     reason = CANCEL_REASON['TIMEOUT']
     order = {'remaining': 1,
+             'id': '125',
              'amount': 1,
              'status': "open"}
     assert not freqtrade.handle_cancel_exit(trade, order, reason)
 
+    # mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order)
+    # assert not freqtrade.handle_cancel_exit(trade, order, reason)
+    # assert trade.open_order_id == '125'
+
 
 @pytest.mark.parametrize("is_short, open_rate, amt", [
     (False, 2.0, 30.0),
@@ -3252,10 +3564,10 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
-        _is_dry_limit_order_filled=MagicMock(return_value=False),
+        _dry_is_price_crossed=MagicMock(return_value=False),
     )
     patch_whitelist(mocker, default_conf_usdt)
     freqtrade = FreqtradeBot(default_conf_usdt)
@@ -3266,14 +3578,14 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     freqtrade.enter_positions()
     rpc_mock.reset_mock()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade.is_short == is_short
     assert trade
     assert freqtrade.strategy.confirm_trade_exit.call_count == 0
 
     # Increase the price and sell it
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt_sell_down if is_short else ticker_usdt_sell_up
     )
     # Prevented sell ...
@@ -3318,6 +3630,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
         'profit_ratio': 0.00493809 if is_short else 0.09451372,
         'stake_currency': 'USDT',
         'fiat_currency': 'USD',
+        'base_currency': 'ETH',
         'sell_reason': ExitType.ROI.value,
         'exit_reason': ExitType.ROI.value,
         'open_date': ANY,
@@ -3335,10 +3648,10 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
-        _is_dry_limit_order_filled=MagicMock(return_value=False),
+        _dry_is_price_crossed=MagicMock(return_value=False),
     )
     patch_whitelist(mocker, default_conf_usdt)
     freqtrade = FreqtradeBot(default_conf_usdt)
@@ -3347,13 +3660,13 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
     # Create some test data
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
 
     # Decrease the price and sell it
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down
     )
     freqtrade.execute_trade_exit(
@@ -3381,6 +3694,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
         'profit_amount': -5.65990099 if is_short else -0.00075,
         'profit_ratio': -0.0945681 if is_short else -1.247e-05,
         'stake_currency': 'USDT',
+        'base_currency': 'ETH',
         'fiat_currency': 'USD',
         'sell_reason': ExitType.STOP_LOSS.value,
         'exit_reason': ExitType.STOP_LOSS.value,
@@ -3404,10 +3718,10 @@ def test_execute_trade_exit_custom_exit_price(
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
-        _is_dry_limit_order_filled=MagicMock(return_value=False),
+        _dry_is_price_crossed=MagicMock(return_value=False),
     )
     config = deepcopy(default_conf_usdt)
     config['custom_price_max_distance_ratio'] = 0.1
@@ -3420,14 +3734,14 @@ def test_execute_trade_exit_custom_exit_price(
     freqtrade.enter_positions()
     rpc_mock.reset_mock()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
     assert freqtrade.strategy.confirm_trade_exit.call_count == 0
 
     # Increase the price and sell it
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt_sell_up
     )
 
@@ -3466,6 +3780,7 @@ def test_execute_trade_exit_custom_exit_price(
         'profit_amount': pytest.approx(profit_amount),
         'profit_ratio': profit_ratio,
         'stake_currency': 'USDT',
+        'base_currency': 'ETH',
         'fiat_currency': 'USD',
         'sell_reason': 'foo',
         'exit_reason': 'foo',
@@ -3485,10 +3800,10 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
-        _is_dry_limit_order_filled=MagicMock(return_value=False),
+        _dry_is_price_crossed=MagicMock(return_value=False),
     )
     patch_whitelist(mocker, default_conf_usdt)
     freqtrade = FreqtradeBot(default_conf_usdt)
@@ -3497,13 +3812,13 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
     # Create some test data
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade.is_short == is_short
     assert trade
 
     # Decrease the price and sell it
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down
     )
 
@@ -3539,6 +3854,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
         'profit_ratio': -0.00501253 if is_short else -0.01493766,
         'stake_currency': 'USDT',
         'fiat_currency': 'USD',
+        'base_currency': 'ETH',
         'sell_reason': ExitType.STOP_LOSS.value,
         'exit_reason': ExitType.STOP_LOSS.value,
         'open_date': ANY,
@@ -3553,8 +3869,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
 def test_execute_trade_exit_sloe_cancel_exception(
         mocker, default_conf_usdt, ticker_usdt, fee, caplog) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
-                 side_effect=InvalidOrderException())
+    mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException())
     mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300))
     create_order_mock = MagicMock(side_effect=[
         {'id': '12345554'},
@@ -3562,7 +3877,7 @@ def test_execute_trade_exit_sloe_cancel_exception(
     ])
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
         create_order=create_order_mock,
@@ -3572,7 +3887,7 @@ def test_execute_trade_exit_sloe_cancel_exception(
     patch_get_signal(freqtrade)
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     PairLock.session = MagicMock()
 
     freqtrade.config['dry_run'] = False
@@ -3581,7 +3896,7 @@ def test_execute_trade_exit_sloe_cancel_exception(
     freqtrade.execute_trade_exit(trade=trade, limit=1234,
                                  exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS))
     assert create_order_mock.call_count == 2
-    assert log_has('Could not cancel stoploss order abcd', caplog)
+    assert log_has('Could not cancel stoploss order abcd for pair ETH/USDT', caplog)
 
 
 @pytest.mark.parametrize("is_short", [False, True])
@@ -3593,21 +3908,23 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
     patch_exchange(mocker)
     stoploss = MagicMock(return_value={
         'id': 123,
+        'status': 'open',
         'info': {
             'foo': 'bar'
         }
     })
+    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee')
 
     cancel_order = MagicMock(return_value=True)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
-        stoploss=stoploss,
+        create_stoploss=stoploss,
         cancel_stoploss_order=cancel_order,
-        _is_dry_limit_order_filled=MagicMock(side_effect=[True, False]),
+        _dry_is_price_crossed=MagicMock(side_effect=[True, False]),
     )
 
     freqtrade = FreqtradeBot(default_conf_usdt)
@@ -3617,7 +3934,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
     # Create some test data
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
     trades = [trade]
@@ -3627,7 +3944,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
 
     # Increase the price and sell it
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt_sell_up
     )
 
@@ -3637,7 +3954,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
         exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)
     )
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
     assert cancel_order.call_count == 1
@@ -3651,12 +3968,12 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
-        _is_dry_limit_order_filled=MagicMock(side_effect=[False, True]),
+        _dry_is_price_crossed=MagicMock(side_effect=[False, True]),
     )
 
     stoploss = MagicMock(return_value={
@@ -3666,7 +3983,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
         }
     })
 
-    mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
+    mocker.patch(f'{EXMS}.create_stoploss', stoploss)
 
     freqtrade = FreqtradeBot(default_conf_usdt)
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@@ -3675,7 +3992,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
     # Create some test data
     freqtrade.enter_positions()
     freqtrade.manage_open_orders()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trades = [trade]
     assert trade.stoploss_order_id is None
 
@@ -3694,18 +4011,18 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(
         "lastTradeTimestamp": None,
         "symbol": "BTC/USDT",
         "type": "stop_loss_limit",
-        "side": "sell",
+        "side": "buy" if is_short else "sell",
         "price": 1.08801,
-        "amount": 90.99181074,
-        "cost": 99.0000000032274,
+        "amount": trade.amount,
+        "cost": 1.08801 * trade.amount,
         "average": 1.08801,
-        "filled": 90.99181074,
+        "filled": trade.amount,
         "remaining": 0.0,
         "status": "closed",
         "fee": None,
         "trades": None
     })
-    mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_executed)
+    mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed)
 
     freqtrade.exit_positions(trades)
     assert trade.stoploss_order_id is None
@@ -3748,10 +4065,10 @@ def test_execute_trade_exit_market_order(
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
-        _is_dry_limit_order_filled=MagicMock(return_value=True),
+        _dry_is_price_crossed=MagicMock(return_value=True),
         get_funding_fees=MagicMock(side_effect=ExchangeError()),
     )
     patch_whitelist(mocker, default_conf_usdt)
@@ -3761,15 +4078,15 @@ def test_execute_trade_exit_market_order(
     # Create some test data
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
 
     # Increase the price and sell it
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt_sell_up,
-        _is_dry_limit_order_filled=MagicMock(return_value=False),
+        _dry_is_price_crossed=MagicMock(return_value=False),
     )
     freqtrade.config['order_types']['exit'] = 'market'
 
@@ -3804,6 +4121,7 @@ def test_execute_trade_exit_market_order(
         'profit_amount': pytest.approx(profit_amount),
         'profit_ratio': profit_ratio,
         'stake_currency': 'USDT',
+        'base_currency': 'ETH',
         'fiat_currency': 'USD',
         'sell_reason': ExitType.ROI.value,
         'exit_reason': ExitType.ROI.value,
@@ -3823,7 +4141,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds')
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
         create_order=MagicMock(side_effect=[
@@ -3836,13 +4154,13 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
     # Create some test data
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
 
     # Increase the price and sell it
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt_sell_up
     )
 
@@ -3877,7 +4195,7 @@ def test_exit_profit_only(
     patch_exchange(mocker)
     eside = entry_side(is_short)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': bid,
             'ask': ask,
@@ -3900,11 +4218,11 @@ def test_exit_profit_only(
     if exit_type == ExitType.EXIT_SIGNAL.value:
         freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     else:
-        freqtrade.strategy.stop_loss_reached = MagicMock(return_value=ExitCheckTuple(
+        freqtrade.strategy.ft_stoploss_reached = MagicMock(return_value=ExitCheckTuple(
             exit_type=ExitType.NONE))
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade.is_short == is_short
     oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
     trade.update_order(limit_order[eside])
@@ -3928,7 +4246,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_ope
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 0.00002172,
             'ask': 0.00002173,
@@ -3947,7 +4265,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_ope
 
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     amnt = trade.amount
 
     oobj = Order.parse_from_ccxt_object(limit_order['buy'], limit_order['buy']['symbol'], 'buy')
@@ -4005,7 +4323,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
     )
@@ -4015,13 +4333,13 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
     # Create some test data
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     assert trade
 
     # Decrease the price and sell it
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt_sell_down
     )
 
@@ -4050,7 +4368,7 @@ def test_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_
     patch_exchange(mocker)
     eside = entry_side(is_short)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 2.19,
             'ask': 2.2,
@@ -4070,7 +4388,7 @@ def test_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limit_order_
 
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     oobj = Order.parse_from_ccxt_object(
         limit_order[eside], limit_order[eside]['symbol'], eside)
@@ -4101,7 +4419,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 2.0,
             'ask': 2.0,
@@ -4120,12 +4438,12 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
 
     freqtrade.enter_positions()
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade.is_short == is_short
     assert freqtrade.handle_trade(trade) is False
 
     # Raise praise into profits
-    mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
+    mocker.patch(f'{EXMS}.fetch_ticker',
                  MagicMock(return_value={
                      'bid': 2.0 * val1,
                      'ask': 2.0 * val1,
@@ -4136,7 +4454,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
     assert freqtrade.handle_trade(trade) is False
     caplog.clear()
     # Price fell
-    mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
+    mocker.patch(f'{EXMS}.fetch_ticker',
                  MagicMock(return_value={
                      'bid': 2.0 * val2,
                      'ask': 2.0 * val2,
@@ -4171,7 +4489,7 @@ def test_trailing_stop_loss_positive(
     patch_exchange(mocker)
     eside = entry_side(is_short)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': enter_price - (-0.01 if is_short else 0.01),
             'ask': enter_price - (-0.01 if is_short else 0.01),
@@ -4195,7 +4513,7 @@ def test_trailing_stop_loss_positive(
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade.is_short == is_short
     oobj = Order.parse_from_ccxt_object(limit_order[eside], limit_order[eside]['symbol'], eside)
     trade.update_order(limit_order[eside])
@@ -4206,7 +4524,7 @@ def test_trailing_stop_loss_positive(
 
     # Raise ticker_usdt above buy price
     mocker.patch(
-        'freqtrade.exchange.Exchange.fetch_ticker',
+        f'{EXMS}.fetch_ticker',
         MagicMock(return_value={
             'bid': enter_price + (-0.06 if is_short else 0.06),
             'ask': enter_price + (-0.06 if is_short else 0.06),
@@ -4228,7 +4546,7 @@ def test_trailing_stop_loss_positive(
     caplog.clear()
 
     mocker.patch(
-        'freqtrade.exchange.Exchange.fetch_ticker',
+        f'{EXMS}.fetch_ticker',
         MagicMock(return_value={
             'bid': enter_price + (-0.135 if is_short else 0.125),
             'ask': enter_price + (-0.135 if is_short else 0.125),
@@ -4244,7 +4562,7 @@ def test_trailing_stop_loss_positive(
     assert log_has("ETH/USDT - Adjusting stoploss...", caplog)
 
     mocker.patch(
-        'freqtrade.exchange.Exchange.fetch_ticker',
+        f'{EXMS}.fetch_ticker',
         MagicMock(return_value={
             'bid': enter_price + (-0.02 if is_short else 0.02),
             'ask': enter_price + (-0.02 if is_short else 0.02),
@@ -4269,7 +4587,7 @@ def test_disable_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limi
     patch_exchange(mocker)
     eside = entry_side(is_short)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 2.0,
             'ask': 2.0,
@@ -4281,7 +4599,7 @@ def test_disable_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limi
             {'id': 1234553383}
         ]),
         get_fee=fee,
-        _is_dry_limit_order_filled=MagicMock(return_value=False),
+        _dry_is_price_crossed=MagicMock(return_value=False),
     )
     default_conf_usdt['exit_pricing'] = {
         'ignore_roi_if_entry_signal': False
@@ -4292,7 +4610,7 @@ def test_disable_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limi
 
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
 
     oobj = Order.parse_from_ccxt_object(
@@ -4310,7 +4628,7 @@ def test_disable_ignore_roi_if_entry_signal(default_conf_usdt, limit_order, limi
 
 def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fee, fee, caplog,
                                mocker):
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order)
     amount = sum(x['amount'] for x in trades_for_order)
     trade = Trade(
         pair='LTC/ETH',
@@ -4336,7 +4654,7 @@ def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fe
 
 def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_order_fee, fee,
                                     caplog, mocker):
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order)
     walletmock = mocker.patch('freqtrade.wallets.Wallets.update')
     mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=8.1122)
     amount = sum(x['amount'] for x in trades_for_order)
@@ -4361,7 +4679,7 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord
 
 
 def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mocker, fee):
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
 
     amount = buy_order_fee['amount']
     trade = Trade(
@@ -4415,7 +4733,7 @@ def test_get_real_amount(
     buy_order['fee'] = fee_par
     trades_for_order[0]['fee'] = fee_par
 
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order)
     amount = sum(x['amount'] for x in trades_for_order)
     trade = Trade(
         pair='LTC/ETH',
@@ -4429,7 +4747,7 @@ def test_get_real_amount(
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
 
     if not use_ticker_usdt_rate:
-        mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', side_effect=ExchangeError)
+        mocker.patch(f'{EXMS}.fetch_ticker', side_effect=ExchangeError)
 
     caplog.clear()
     order_obj = Order.parse_from_ccxt_object(buy_order_fee, 'LTC/ETH', 'buy')
@@ -4461,7 +4779,7 @@ def test_get_real_amount_multi(
     if fee_currency:
         trades_for_order[0]['fee']['currency'] = fee_currency
 
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order)
     amount = float(sum(x['amount'] for x in trades_for_order))
     default_conf_usdt['stake_currency'] = "ETH"
 
@@ -4478,8 +4796,8 @@ def test_get_real_amount_multi(
     # Fake markets entry to enable fee parsing
     markets['BNB/ETH'] = markets['ETH/USDT']
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
+    mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
+    mocker.patch(f'{EXMS}.fetch_ticker',
                  return_value={'ask': 0.19, 'last': 0.2})
 
     # Amount is reduced by "fee"
@@ -4508,7 +4826,7 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_
     limit_buy_order_usdt = deepcopy(buy_order_fee)
     limit_buy_order_usdt['fee'] = {'cost': 0.004}
 
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
     amount = float(sum(x['amount'] for x in trades_for_order))
     trade = Trade(
         pair='LTC/ETH',
@@ -4529,9 +4847,9 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_
 def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_doublefee,
                                     fee, mocker):
 
-    tfo_mock = mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
-    mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', return_value='BNB/USDT')
-    mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'last': 200})
+    tfo_mock = mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[])
+    mocker.patch(f'{EXMS}.get_valid_pair_combination', return_value='BNB/USDT')
+    mocker.patch(f'{EXMS}.fetch_ticker', return_value={'last': 200})
     trade = Trade(
         pair='LTC/USDT',
         amount=30.0,
@@ -4557,7 +4875,7 @@ def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_o
     limit_buy_order_usdt = deepcopy(buy_order_fee)
     limit_buy_order_usdt['amount'] = limit_buy_order_usdt['amount'] - 0.001
 
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order)
     amount = float(sum(x['amount'] for x in trades_for_order))
     trade = Trade(
         pair='LTC/ETH',
@@ -4582,7 +4900,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord
     limit_buy_order_usdt = deepcopy(buy_order_fee)
     trades_for_order[0]['amount'] = trades_for_order[0]['amount'] + 1e-15
 
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades_for_order)
     amount = float(sum(x['amount'] for x in trades_for_order))
     trade = Trade(
         pair='LTC/ETH',
@@ -4662,7 +4980,7 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker,
         ]
     }]
 
-    mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades)
+    mocker.patch(f'{EXMS}.get_trades_for_order', return_value=trades)
     amount = float(sum(x['amount'] for x in trades))
     trade = Trade(
         pair='CEL/USDT',
@@ -4744,9 +5062,9 @@ def test_order_book_depth_of_market(
     default_conf_usdt['entry_pricing']['check_depth_of_market']['bids_to_ask_delta'] = delta
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2)
+    mocker.patch(f'{EXMS}.fetch_l2_order_book', order_book_l2)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=limit_order_open[entry_side(is_short)]),
         get_fee=fee,
@@ -4758,7 +5076,7 @@ def test_order_book_depth_of_market(
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     if is_high_delta:
         assert trade is None
     else:
@@ -4769,7 +5087,7 @@ def test_order_book_depth_of_market(
         assert trade.open_date is not None
         assert trade.exchange == 'binance'
 
-        assert len(Trade.query.all()) == 1
+        assert len(Trade.session.scalars(select(Trade)).all()) == 1
 
         # Simulate fulfilled LIMIT_BUY order for trade
         oobj = Order.parse_from_ccxt_object(
@@ -4792,7 +5110,7 @@ def test_order_book_entry_pricing1(mocker, default_conf_usdt, order_book_l2, exc
     patch_exchange(mocker)
     ticker_usdt_mock = MagicMock(return_value={'ask': ask, 'last': last})
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2,
         fetch_ticker=ticker_usdt_mock,
     )
@@ -4820,7 +5138,7 @@ def test_check_depth_of_market(default_conf_usdt, mocker, order_book_l2) -> None
     """
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_l2_order_book=order_book_l2
     )
     default_conf_usdt['telegram']['enabled'] = False
@@ -4841,7 +5159,7 @@ def test_order_book_exit_pricing(
     """
     test order book ask strategy
     """
-    mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', order_book_l2)
+    mocker.patch(f'{EXMS}.fetch_l2_order_book', order_book_l2)
     default_conf_usdt['exchange']['name'] = 'binance'
     default_conf_usdt['exit_pricing']['use_order_book'] = True
     default_conf_usdt['exit_pricing']['order_book_top'] = 1
@@ -4849,7 +5167,7 @@ def test_order_book_exit_pricing(
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
             'ask': 2.2,
@@ -4866,7 +5184,7 @@ def test_order_book_exit_pricing(
 
     freqtrade.enter_positions()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
 
     time.sleep(0.01)  # Race condition fix
@@ -4882,8 +5200,7 @@ def test_order_book_exit_pricing(
     assert freqtrade.handle_trade(trade) is True
     assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0]
 
-    mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book',
-                 return_value={'bids': [[]], 'asks': [[]]})
+    mocker.patch(f'{EXMS}.fetch_l2_order_book', return_value={'bids': [[]], 'asks': [[]]})
     with pytest.raises(PricingError):
         freqtrade.handle_trade(trade)
     assert log_has_re(
@@ -4895,14 +5212,14 @@ def test_startup_state(default_conf_usdt, mocker):
     default_conf_usdt['pairlist'] = {'method': 'VolumePairList',
                                      'config': {'number_assets': 20}
                                      }
-    mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
+    mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
     worker = get_patched_worker(mocker, default_conf_usdt)
     assert worker.freqtrade.state is State.RUNNING
 
 
 def test_startup_trade_reinit(default_conf_usdt, edge_conf, mocker):
 
-    mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
+    mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True))
     reinit_mock = MagicMock()
     mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', reinit_mock)
 
@@ -4927,7 +5244,7 @@ def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_
     default_conf_usdt['tradable_balance_ratio'] = 1.0
     patch_exchange(mocker)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         create_order=MagicMock(return_value=limit_buy_order_usdt_open),
         get_fee=fee,
@@ -4939,7 +5256,7 @@ def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_
 
     n = bot.enter_positions()
     assert n == 2
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
     assert len(trades) == 2
 
     bot.config['max_open_trades'] = 3
@@ -4959,7 +5276,7 @@ def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, lim
                                 is_short, buy_calls, sell_calls):
     default_conf_usdt['cancel_open_orders_on_exit'] = True
     mocker.patch(
-        'freqtrade.exchange.Exchange.fetch_order',
+        f'{EXMS}.fetch_order',
         side_effect=[
             ExchangeError(),
             limit_order[exit_side(is_short)],
@@ -4972,7 +5289,7 @@ def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, lim
 
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     create_mock_trades(fee, is_short=is_short)
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
     assert len(trades) == MOCK_TRADE_COUNT
     freqtrade.cancel_all_open_orders()
     assert buy_mock.call_count == buy_calls
@@ -4988,7 +5305,7 @@ def test_check_for_open_trades(mocker, default_conf_usdt, fee, is_short):
     assert freqtrade.rpc.send_msg.call_count == 0
 
     create_mock_trades(fee, is_short)
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     trade.is_short = is_short
     trade.is_open = True
 
@@ -5015,18 +5332,18 @@ def test_startup_update_open_orders(mocker, default_conf_usdt, fee, caplog, is_s
     matching_buy_order.update({
         'status': 'closed',
     })
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order)
+    mocker.patch(f'{EXMS}.fetch_order', return_value=matching_buy_order)
     freqtrade.startup_update_open_orders()
     # Only stoploss and sell orders are kept open
     assert len(Order.get_open_orders()) == 2
 
     caplog.clear()
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=ExchangeError)
+    mocker.patch(f'{EXMS}.fetch_order', side_effect=ExchangeError)
     freqtrade.startup_update_open_orders()
     assert log_has_re(r"Error updating Order .*", caplog)
 
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=InvalidOrderException)
-    hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_timedout_order')
+    mocker.patch(f'{EXMS}.fetch_order', side_effect=InvalidOrderException)
+    hto_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_order')
     # Orders which are no longer found after X days should be assumed as canceled.
     freqtrade.startup_update_open_orders()
     assert log_has_re(r"Order is older than \d days.*", caplog)
@@ -5070,7 +5387,7 @@ def test_update_trades_without_assigned_fees(mocker, default_conf_usdt, fee, is_
                       'currency': order['symbol'].split('/')[0]}})
         return order
 
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                  side_effect=[
                      patch_with_fee(mock_order_2_sell(is_short=is_short)),
                      patch_with_fee(mock_order_3_sell(is_short=is_short)),
@@ -5130,8 +5447,7 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog, is_sh
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
 
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
-                 return_value={'status': 'open'})
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', return_value={'status': 'open'})
     create_mock_trades(fee, is_short)
     trades = Trade.get_trades().all()
 
@@ -5150,14 +5466,14 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog, is_sh
         stake_amount=60.0,
         fee_open=fee.return_value,
         fee_close=fee.return_value,
-        open_date=arrow.utcnow().datetime,
+        open_date=dt_now(),
         is_open=True,
         amount=30,
         open_rate=2.0,
         exchange='binance',
         is_short=is_short
     )
-    Trade.query.session.add(trade)
+    Trade.session.add(trade)
 
     freqtrade.handle_insufficient_funds(trade)
     # assert log_has_re(r"Trying to reupdate buy fees for .*", caplog)
@@ -5171,7 +5487,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state')
 
-    mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mock_fo = mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                            return_value={'status': 'open'})
 
     def reset_open_orders(trade):
@@ -5257,7 +5573,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
     caplog.clear()
 
     # Test error case
-    mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mock_fo = mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                            side_effect=ExchangeError())
     order = mock_order_5_stoploss(is_short=is_short)
 
@@ -5265,6 +5581,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=dt_now(),
+            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)
@@ -5379,9 +5740,9 @@ def test_update_funding_fees(
     default_conf['trading_mode'] = 'futures'
     default_conf['margin_mode'] = 'isolated'
 
-    date_midnight = arrow.get('2021-09-01 00:00:00').datetime
-    date_eight = arrow.get('2021-09-01 08:00:00').datetime
-    date_sixteen = arrow.get('2021-09-01 16:00:00').datetime
+    date_midnight = dt_utc(2021, 9, 1)
+    date_eight = dt_utc(2021, 9, 1, 8)
+    date_sixteen = dt_utc(2021, 9, 1, 16)
     columns = ['date', 'open', 'high', 'low', 'close', 'volume']
     # 16:00 entry is actually never used
     # But should be kept in the test to ensure we're filtering correctly.
@@ -5437,11 +5798,10 @@ def test_update_funding_fees(
 
         return ret
 
-    mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv',
-                 side_effect=refresh_latest_ohlcv_mock)
+    mocker.patch(f'{EXMS}.refresh_latest_ohlcv', side_effect=refresh_latest_ohlcv_mock)
 
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_rate=enter_rate_mock,
         fetch_ticker=MagicMock(return_value={
             'bid': 1.9,
@@ -5465,7 +5825,7 @@ def test_update_funding_fees(
     assert len(trades) == 3
     for trade in trades:
         assert pytest.approx(trade.funding_fees) == 0
-    mocker.patch('freqtrade.exchange.Exchange.create_order', return_value=open_exit_order)
+    mocker.patch(f'{EXMS}.create_order', return_value=open_exit_order)
     time_machine.move_to("2021-09-01 08:00:00 +00:00")
     if schedule_off:
         for trade in trades:
@@ -5495,7 +5855,7 @@ def test_update_funding_fees(
 
 
 def test_update_funding_fees_error(mocker, default_conf, caplog):
-    mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', side_effect=ExchangeError())
+    mocker.patch(f'{EXMS}.get_funding_fees', side_effect=ExchangeError())
     default_conf['trading_mode'] = 'futures'
     default_conf['margin_mode'] = 'isolated'
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
@@ -5520,7 +5880,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
     stake_amount = 10
     buy_rate_mock = MagicMock(return_value=bid)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_rate=buy_rate_mock,
         fetch_ticker=MagicMock(return_value={
             'bid': 10,
@@ -5549,17 +5909,16 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
         'id': '650',
         'order_id': '650'
     }
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=closed_successful_buy_order))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_successful_buy_order))
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                  MagicMock(return_value=closed_successful_buy_order))
     assert freqtrade.execute_entry(pair, stake_amount)
     # Should create an closed trade with an no open order id
     # Order is filled and trade is open
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 1
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.is_open is True
     assert trade.open_order_id is None
@@ -5569,7 +5928,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
     # Assume it does nothing since order is closed and trade is open
     freqtrade.update_trades_without_assigned_fees()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.is_open is True
     assert trade.open_order_id is None
@@ -5579,7 +5938,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
 
     freqtrade.manage_open_orders()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.is_open is True
     assert trade.open_order_id is None
@@ -5601,16 +5960,14 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
         'id': '651',
         'order_id': '651'
     }
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=open_dca_order_1))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
-                 MagicMock(return_value=open_dca_order_1))
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=open_dca_order_1))
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', MagicMock(return_value=open_dca_order_1))
     assert freqtrade.execute_entry(pair, stake_amount, trade=trade)
 
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 2
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.open_order_id == '651'
     assert trade.open_rate == 11
@@ -5635,19 +5992,19 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
 
     # Assume it does nothing since order is still open
     fetch_order_mm = MagicMock(side_effect=make_sure_its_651)
-    mocker.patch('freqtrade.exchange.Exchange.create_order', fetch_order_mm)
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', fetch_order_mm)
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', fetch_order_mm)
+    mocker.patch(f'{EXMS}.create_order', fetch_order_mm)
+    mocker.patch(f'{EXMS}.fetch_order', fetch_order_mm)
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order', fetch_order_mm)
     freqtrade.update_trades_without_assigned_fees()
 
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 2
     # Assert that the trade is found as open and without fees
     trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
     assert len(trades) == 1
     # Assert trade is as expected
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.open_order_id == '651'
     assert trade.open_rate == 11
@@ -5674,26 +6031,24 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
         'ft_is_open': False,
         'id': '651',
         'order_id': '651',
-        'datetime': arrow.utcnow().isoformat(),
+        'datetime': dt_now().isoformat(),
     }
 
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=closed_dca_order_1))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
-                 MagicMock(return_value=closed_dca_order_1))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_dca_order_1))
+    mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_dca_order_1))
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                  MagicMock(return_value=closed_dca_order_1))
     freqtrade.manage_open_orders()
 
     # Assert trade is as expected (averaged dca)
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.open_order_id is None
     assert pytest.approx(trade.open_rate) == 9.90909090909
     assert trade.amount == 22
     assert pytest.approx(trade.stake_amount) == 218
 
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 2
 
@@ -5721,23 +6076,21 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
         'id': '652',
         'order_id': '652'
     }
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=closed_dca_order_2))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
-                 MagicMock(return_value=closed_dca_order_2))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_dca_order_2))
+    mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_dca_order_2))
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                  MagicMock(return_value=closed_dca_order_2))
     assert freqtrade.execute_entry(pair, stake_amount, trade=trade)
 
     # Assert trade is as expected (averaged dca)
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.open_order_id is None
     assert pytest.approx(trade.open_rate) == 8.729729729729
     assert trade.amount == 37
     assert trade.stake_amount == 323
 
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 3
 
@@ -5759,18 +6112,16 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
         'id': '653',
         'order_id': '653'
     }
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=closed_sell_dca_order_1))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
-                 MagicMock(return_value=closed_sell_dca_order_1))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_sell_dca_order_1))
+    mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_sell_dca_order_1))
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                  MagicMock(return_value=closed_sell_dca_order_1))
     assert freqtrade.execute_trade_exit(trade=trade, limit=8,
                                         exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
                                         sub_trade_amt=15)
 
     # Assert trade is as expected (averaged dca)
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.open_order_id is None
     assert trade.is_open
@@ -5778,7 +6129,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None:
     assert trade.stake_amount == 192.05405405405406
     assert pytest.approx(trade.open_rate) == 8.729729729729
 
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 4
 
@@ -5809,7 +6160,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
     amount = 100
     buy_rate_mock = MagicMock(return_value=bid)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_rate=buy_rate_mock,
         fetch_ticker=MagicMock(return_value={
             'bid': 10,
@@ -5837,17 +6188,16 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
         'id': '600',
         'order_id': '600'
     }
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=closed_successful_buy_order))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_successful_buy_order))
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                  MagicMock(return_value=closed_successful_buy_order))
     assert freqtrade.execute_entry(pair, amount)
     # Should create an closed trade with an no open order id
     # Order is filled and trade is open
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 1
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.is_open is True
     assert trade.open_order_id is None
@@ -5857,7 +6207,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
     # Assume it does nothing since order is closed and trade is open
     freqtrade.update_trades_without_assigned_fees()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.is_open is True
     assert trade.open_order_id is None
@@ -5867,7 +6217,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
 
     freqtrade.manage_open_orders()
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.is_open is True
     assert trade.open_order_id is None
@@ -5892,11 +6242,9 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
         'id': '601',
         'order_id': '601'
     }
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=closed_sell_dca_order_1))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
-                 MagicMock(return_value=closed_sell_dca_order_1))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_sell_dca_order_1))
+    mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_sell_dca_order_1))
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                  MagicMock(return_value=closed_sell_dca_order_1))
     assert freqtrade.execute_trade_exit(trade=trade, limit=ask,
                                         exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
@@ -5905,7 +6253,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
     assert len(trades) == 1
     # Assert trade is as expected (averaged dca)
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.open_order_id is None
     assert trade.amount == 50
@@ -5914,7 +6262,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
     assert pytest.approx(trade.realized_profit) == -152.375
     assert pytest.approx(trade.close_profit_abs) == -152.375
 
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 2
     # Make sure the closed order is found as the second order.
@@ -5938,18 +6286,16 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
         'id': '602',
         'order_id': '602'
     }
-    mocker.patch('freqtrade.exchange.Exchange.create_order',
-                 MagicMock(return_value=closed_sell_dca_order_2))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
-                 MagicMock(return_value=closed_sell_dca_order_2))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+    mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_sell_dca_order_2))
+    mocker.patch(f'{EXMS}.fetch_order', MagicMock(return_value=closed_sell_dca_order_2))
+    mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                  MagicMock(return_value=closed_sell_dca_order_2))
     assert freqtrade.execute_trade_exit(trade=trade, limit=ask,
                                         exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
                                         sub_trade_amt=amount)
     # Assert trade is as expected (averaged dca)
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.open_order_id is None
     assert trade.amount == 50
@@ -5958,7 +6304,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None:
     # Trade fully realized
     assert pytest.approx(trade.realized_profit) == 94.25
     assert pytest.approx(trade.close_profit_abs) == 94.25
-    orders = Order.query.all()
+    orders = Order.session.scalars(select(Order)).all()
     assert orders
     assert len(orders) == 3
 
@@ -6005,7 +6351,7 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None:
         price = order[2]
         price_mock = MagicMock(return_value=price)
         mocker.patch.multiple(
-            'freqtrade.exchange.Exchange',
+            EXMS,
             get_rate=price_mock,
             fetch_ticker=MagicMock(return_value={
                 'bid': 10,
@@ -6032,9 +6378,8 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None:
             'id': f'60{idx}',
             'order_id': f'60{idx}'
         }
-        mocker.patch('freqtrade.exchange.Exchange.create_order',
-                     MagicMock(return_value=closed_successful_order))
-        mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order',
+        mocker.patch(f'{EXMS}.create_order', MagicMock(return_value=closed_successful_order))
+        mocker.patch(f'{EXMS}.fetch_order_or_stoploss_order',
                      MagicMock(return_value=closed_successful_order))
         if order[0] == 'buy':
             assert freqtrade.execute_entry(pair, amount, trade=trade)
@@ -6044,11 +6389,11 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None:
                 exit_check=ExitCheckTuple(exit_type=ExitType.PARTIAL_EXIT),
                 sub_trade_amt=amount)
 
-        orders1 = Order.query.all()
+        orders1 = Order.session.scalars(select(Order)).all()
         assert orders1
         assert len(orders1) == idx + 1
 
-        trade = Trade.query.first()
+        trade = Trade.session.scalars(select(Trade)).first()
         assert trade
         if idx < len(data) - 1:
             assert trade.is_open is True
@@ -6063,7 +6408,7 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None:
         order_obj = trade.select_order(order[0], False)
         assert order_obj.order_id == f'60{idx}'
 
-    trade = Trade.query.first()
+    trade = Trade.session.scalars(select(Trade)).first()
     assert trade
     assert trade.open_order_id is None
     assert trade.is_open is False
@@ -6092,7 +6437,7 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     buy_rate_mock = MagicMock(return_value=10)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_rate=buy_rate_mock,
         fetch_ticker=MagicMock(return_value={
             'bid': 10,
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 01a2801ad..2949f1ef2 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,12 +1,13 @@
 from unittest.mock import MagicMock
 
 import pytest
+from sqlalchemy import select
 
 from freqtrade.enums import ExitCheckTuple, ExitType, TradingMode
 from freqtrade.persistence import Trade
 from freqtrade.persistence.models import Order
 from freqtrade.rpc.rpc import RPC
-from tests.conftest import get_patched_freqtradebot, log_has_re, patch_get_signal
+from tests.conftest import EXMS, get_patched_freqtradebot, log_has_re, patch_get_signal
 
 
 def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
@@ -34,7 +35,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
         "type": "stop_loss_limit",
         "side": "sell",
         "price": 1.08801,
-        "amount": 90.99181074,
+        "amount": 91.07468123,
         "cost": 0.0,
         "average": 0.0,
         "filled": 0.0,
@@ -48,17 +49,18 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
     stoploss_order_closed['filled'] = stoploss_order_closed['amount']
 
     # Sell first trade based on stoploss, keep 2nd and 3rd trade open
+    stop_orders = [stoploss_order_closed, stoploss_order_open, stoploss_order_open]
     stoploss_order_mock = MagicMock(
-        side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
+        side_effect=stop_orders)
     # Sell 3rd trade (not called for the first trade)
     should_sell_mock = MagicMock(side_effect=[
         [],
         [ExitCheckTuple(exit_type=ExitType.EXIT_SIGNAL)]]
     )
     cancel_order_mock = MagicMock()
-    mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
+        create_stoploss=stoploss,
         fetch_ticker=ticker,
         get_fee=fee,
         amount_to_precision=lambda s, x, y: y,
@@ -73,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
@@ -91,14 +94,15 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
     assert freqtrade.strategy.confirm_trade_exit.call_count == 0
     wallets_mock.reset_mock()
 
-    trades = Trade.query.all()
-    # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state)
-    for trade in trades:
-        stoploss_order_closed['id'] = '3'
-        oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss')
+    trades = Trade.session.scalars(select(Trade)).all()
+    # Make sure stoploss-order is open and trade is bought
+    for idx, trade in enumerate(trades):
+        stop_order = stop_orders[idx]
+        stop_order['id'] = f"stop{idx}"
+        oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss')
 
         trade.orders.append(oobj)
-        trade.stoploss_order_id = '3'
+        trade.stoploss_order_id = f"stop{idx}"
         trade.open_order_id = None
 
     n = freqtrade.exit_positions(trades)
@@ -147,7 +151,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
     default_conf['telegram']['enabled'] = True
     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker,
         get_fee=fee,
         amount_to_precision=lambda s, x, y: y,
@@ -179,13 +183,13 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
     n = freqtrade.enter_positions()
     assert n == 4
 
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
     assert len(trades) == 4
     assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1
 
     rpc._rpc_force_entry('TKN/BTC', None)
 
-    trades = Trade.query.all()
+    trades = Trade.session.scalars(select(Trade)).all()
     assert len(trades) == 5
 
     for trade in trades:
@@ -217,7 +221,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
 
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
     )
@@ -239,7 +243,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
     # Reduce bid amount
     ticker_usdt_modif = ticker_usdt.return_value
     ticker_usdt_modif['bid'] = ticker_usdt_modif['bid'] * 0.995
-    mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif)
+    mocker.patch(f'{EXMS}.fetch_ticker', return_value=ticker_usdt_modif)
 
     # additional buy order
     freqtrade.process()
@@ -286,7 +290,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
 
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
         amount_to_precision=lambda s, x, y: round(y, 4),
@@ -311,7 +315,7 @@ def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None:
     # Reduce bid amount
     ticker_usdt_modif = ticker_usdt.return_value
     ticker_usdt_modif['ask'] = ticker_usdt_modif['ask'] * 1.004
-    mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif)
+    mocker.patch(f'{EXMS}.fetch_ticker', return_value=ticker_usdt_modif)
 
     # additional buy order
     freqtrade.process()
@@ -361,16 +365,16 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
 
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
     )
-    mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
-    mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10)
-    mocker.patch("freqtrade.exchange.Exchange.get_funding_fees", return_value=0)
-    mocker.patch("freqtrade.exchange.Exchange.get_maintenance_ratio_and_amt", return_value=(0, 0))
+    mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False)
+    mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
+    mocker.patch(f"{EXMS}.get_funding_fees", return_value=0)
+    mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0, 0))
 
     patch_get_signal(freqtrade)
     freqtrade.strategy.custom_entry_price = lambda **kwargs: ticker_usdt['ask'] * 0.96
@@ -385,12 +389,12 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
     assert trade.open_order_id is not None
     assert pytest.approx(trade.stake_amount) == 60
     assert trade.open_rate == 1.96
-    assert trade.stop_loss_pct is None
-    assert trade.stop_loss == 0.0
+    assert trade.stop_loss_pct == -0.1
+    assert pytest.approx(trade.stop_loss) == trade.open_rate * (1 - 0.1 / leverage)
+    assert pytest.approx(trade.initial_stop_loss) == trade.open_rate * (1 - 0.1 / leverage)
+    assert trade.initial_stop_loss_pct == -0.1
     assert trade.leverage == leverage
     assert trade.stake_amount == 60
-    assert trade.initial_stop_loss == 0.0
-    assert trade.initial_stop_loss_pct is None
     # No adjustment
     freqtrade.process()
     trade = Trade.get_trades().first()
@@ -406,14 +410,14 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
     assert trade.open_order_id is not None
     # Open rate is not adjusted yet
     assert trade.open_rate == 1.96
-    assert trade.stop_loss_pct is None
-    assert trade.stop_loss == 0.0
+    assert trade.stop_loss_pct == -0.1
+    assert pytest.approx(trade.stop_loss) == trade.open_rate * (1 - 0.1 / leverage)
+    assert pytest.approx(trade.initial_stop_loss) == trade.open_rate * (1 - 0.1 / leverage)
     assert trade.stake_amount == 60
-    assert trade.initial_stop_loss == 0.0
-    assert trade.initial_stop_loss_pct is None
+    assert trade.initial_stop_loss_pct == -0.1
 
     # Fill order
-    mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
+    mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True)
     freqtrade.process()
     trade = Trade.get_trades().first()
     assert len(trade.orders) == 2
@@ -423,12 +427,12 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
     assert pytest.approx(trade.stake_amount) == 60
     assert trade.stop_loss_pct == -0.1
     assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage)
-    assert pytest.approx(trade.initial_stop_loss) == 1.99 * (1 - 0.1 / leverage)
+    assert pytest.approx(trade.initial_stop_loss) == 1.96 * (1 - 0.1 / leverage)
     assert trade.initial_stop_loss_pct == -0.1
 
     # 2nd order - not filling
     freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120)
-    mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=False)
+    mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False)
 
     freqtrade.process()
     trade = Trade.get_trades().first()
@@ -452,7 +456,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker)
 
     # Fill DCA order
     freqtrade.strategy.adjust_trade_position = MagicMock(return_value=None)
-    mocker.patch('freqtrade.exchange.Exchange._is_dry_limit_order_filled', return_value=True)
+    mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True)
     freqtrade.strategy.adjust_entry_price = MagicMock(side_effect=ValueError)
 
     freqtrade.process()
@@ -477,14 +481,14 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     freqtrade.trading_mode = TradingMode.FUTURES
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker_usdt,
         get_fee=fee,
         amount_to_precision=lambda s, x, y: y,
         price_to_precision=lambda s, x, y: y,
         get_min_pair_stake_amount=MagicMock(return_value=10),
     )
-    mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10)
+    mocker.patch(f"{EXMS}.get_max_leverage", return_value=10)
 
     patch_get_signal(freqtrade)
     freqtrade.strategy.leverage = MagicMock(return_value=leverage)
@@ -532,8 +536,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera
     assert trade.is_open
 
     # use amount that would trunc to 0.0 once selling
-    mocker.patch("freqtrade.exchange.Exchange.amount_to_contract_precision",
-                 lambda s, p, v: round(v, 1))
+    mocker.patch(f"{EXMS}.amount_to_contract_precision", lambda s, p, v: round(v, 1))
     freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-0.01)
     freqtrade.process()
     trade = Trade.get_trades().first()
diff --git a/tests/test_misc.py b/tests/test_misc.py
index 2da45bad9..03a236d73 100644
--- a/tests/test_misc.py
+++ b/tests/test_misc.py
@@ -5,13 +5,14 @@ from copy import deepcopy
 from pathlib import Path
 from unittest.mock import MagicMock
 
+import pandas as pd
 import pytest
 
 from freqtrade.misc import (dataframe_to_json, decimals_per_coin, deep_merge_dicts, file_dump_json,
                             file_load_json, format_ms_time, json_to_dataframe, pair_to_filename,
                             parse_db_uri_for_logging, plural, render_template,
                             render_template_with_fallback, round_coin_value, safe_value_fallback,
-                            safe_value_fallback2, shorten_date)
+                            safe_value_fallback2)
 
 
 def test_decimals_per_coin():
@@ -38,14 +39,8 @@ def test_round_coin_value():
     assert round_coin_value(222.2, 'USDT', False, True) == '222.200'
 
 
-def test_shorten_date() -> None:
-    str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
-    str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
-    assert shorten_date(str_data) == str_shorten_data
-
-
 def test_file_dump_json(mocker) -> None:
-    file_open = mocker.patch('freqtrade.misc.open', MagicMock())
+    file_open = mocker.patch('freqtrade.misc.Path.open', MagicMock())
     json_dump = mocker.patch('rapidjson.dump', MagicMock())
     file_dump_json(Path('somefile'), [1, 2, 3])
     assert file_open.call_count == 1
@@ -231,3 +226,7 @@ def test_dataframe_json(ohlcv_history):
     assert len(ohlcv_history) == len(dataframe)
 
     assert_frame_equal(ohlcv_history, dataframe)
+    ohlcv_history.at[1, 'date'] = pd.NaT
+    json = dataframe_to_json(ohlcv_history)
+
+    dataframe = json_to_dataframe(json)
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
index 7662ea7f1..377caf59c 100644
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -1,5 +1,4 @@
 from copy import deepcopy
-from pathlib import Path
 from unittest.mock import MagicMock
 
 import pandas as pd
@@ -45,7 +44,6 @@ def test_init_plotscript(default_conf, mocker, testdatadir):
     default_conf['timerange'] = "20180110-20180112"
     default_conf['trade_source'] = "file"
     default_conf['timeframe'] = "5m"
-    default_conf["datadir"] = testdatadir
     default_conf['exportfilename'] = testdatadir / "backtest-result.json"
     supported_markets = ["TRX/BTC", "ADA/BTC"]
     ret = init_plotscript(default_conf, supported_markets)
@@ -283,13 +281,13 @@ def test_generate_Plot_filename():
     assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html"
 
 
-def test_generate_plot_file(mocker, caplog):
+def test_generate_plot_file(mocker, caplog, user_dir):
     fig = generate_empty_figure()
     plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
     store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html",
-                    directory=Path("user_data/plot"))
+                    directory=user_dir / "plot")
 
-    expected_fn = str(Path("user_data/plot/freqtrade-plot-UNITTEST_BTC-5m.html"))
+    expected_fn = str(user_dir / "plot/freqtrade-plot-UNITTEST_BTC-5m.html")
     assert plot_mock.call_count == 1
     assert plot_mock.call_args[0][0] == fig
     assert (plot_mock.call_args_list[0][1]['filename']
@@ -394,7 +392,6 @@ def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
     patch_exchange(mocker)
 
     default_conf['trade_source'] = 'file'
-    default_conf["datadir"] = testdatadir
     default_conf['exportfilename'] = testdatadir / "backtest-result.json"
     default_conf['indicators1'] = ["sma5", "ema10"]
     default_conf['indicators2'] = ["macd"]
@@ -451,7 +448,6 @@ def test_start_plot_profit_error(mocker):
 def test_plot_profit(default_conf, mocker, testdatadir):
     patch_exchange(mocker)
     default_conf['trade_source'] = 'file'
-    default_conf['datadir'] = testdatadir
     default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json'
     default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC']
 
diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py
new file mode 100644
index 000000000..3b48c952c
--- /dev/null
+++ b/tests/test_strategy_updater.py
@@ -0,0 +1,214 @@
+# pragma pylint: disable=missing-docstring, protected-access, invalid-name
+
+import re
+import shutil
+import sys
+from pathlib import Path
+
+import pytest
+
+from freqtrade.commands.strategy_utils_commands import start_strategy_update
+from freqtrade.strategy.strategyupdater import StrategyUpdater
+from tests.conftest import get_args
+
+
+if sys.version_info < (3, 9):
+    pytest.skip("StrategyUpdater is not compatible with Python 3.8", allow_module_level=True)
+
+
+def test_strategy_updater_start(user_dir, capsys) -> None:
+    # Effective test without mocks.
+    teststrats = Path(__file__).parent / 'strategy/strats'
+    tmpdirp = Path(user_dir) / "strategies"
+    tmpdirp.mkdir(parents=True, exist_ok=True)
+    shutil.copy(teststrats / 'strategy_test_v2.py', tmpdirp)
+    old_code = (teststrats / 'strategy_test_v2.py').read_text()
+
+    args = [
+        "strategy-updater",
+        "--userdir",
+        str(user_dir),
+        "--strategy-list",
+        "StrategyTestV2"
+         ]
+    pargs = get_args(args)
+    pargs['config'] = None
+
+    start_strategy_update(pargs)
+
+    assert Path(user_dir / "strategies_orig_updater").exists()
+    # Backup file exists
+    assert Path(user_dir / "strategies_orig_updater" / 'strategy_test_v2.py').exists()
+    # updated file exists
+    new_file = Path(tmpdirp / 'strategy_test_v2.py')
+    assert new_file.exists()
+    new_code = new_file.read_text()
+    assert 'INTERFACE_VERSION = 3' in new_code
+    assert 'INTERFACE_VERSION = 2' in old_code
+    captured = capsys.readouterr()
+
+    assert 'Conversion of strategy_test_v2.py started.' in captured.out
+    assert re.search(r'Conversion of strategy_test_v2\.py took .* seconds', captured.out)
+
+
+def test_strategy_updater_methods(default_conf, caplog) -> None:
+
+    instance_strategy_updater = StrategyUpdater()
+    modified_code1 = instance_strategy_updater.update_code("""
+class testClass(IStrategy):
+    def populate_buy_trend():
+        pass
+    def populate_sell_trend():
+        pass
+    def check_buy_timeout():
+        pass
+    def check_sell_timeout():
+        pass
+    def custom_sell():
+        pass
+""")
+
+    assert "populate_entry_trend" in modified_code1
+    assert "populate_exit_trend" in modified_code1
+    assert "check_entry_timeout" in modified_code1
+    assert "check_exit_timeout" in modified_code1
+    assert "custom_exit" in modified_code1
+    assert "INTERFACE_VERSION = 3" in modified_code1
+
+
+def test_strategy_updater_params(default_conf, caplog) -> None:
+    instance_strategy_updater = StrategyUpdater()
+
+    modified_code2 = instance_strategy_updater.update_code("""
+ticker_interval = '15m'
+buy_some_parameter = IntParameter(space='buy')
+sell_some_parameter = IntParameter(space='sell')
+""")
+
+    assert "timeframe" in modified_code2
+    # check for not editing hyperopt spaces
+    assert "space='buy'" in modified_code2
+    assert "space='sell'" in modified_code2
+
+
+def test_strategy_updater_constants(default_conf, caplog) -> None:
+    instance_strategy_updater = StrategyUpdater()
+    modified_code3 = instance_strategy_updater.update_code("""
+use_sell_signal = True
+sell_profit_only = True
+sell_profit_offset = True
+ignore_roi_if_buy_signal = True
+forcebuy_enable = True
+""")
+
+    assert "use_exit_signal" in modified_code3
+    assert "exit_profit_only" in modified_code3
+    assert "exit_profit_offset" in modified_code3
+    assert "ignore_roi_if_entry_signal" in modified_code3
+    assert "force_entry_enable" in modified_code3
+
+
+def test_strategy_updater_df_columns(default_conf, caplog) -> None:
+    instance_strategy_updater = StrategyUpdater()
+    modified_code = instance_strategy_updater.update_code("""
+dataframe.loc[reduce(lambda x, y: x & y, conditions), ["buy", "buy_tag"]] = (1, "buy_signal_1")
+dataframe.loc[reduce(lambda x, y: x & y, conditions), 'sell'] = 1
+""")
+
+    assert "enter_long" in modified_code
+    assert "exit_long" in modified_code
+    assert "enter_tag" in modified_code
+
+
+def test_strategy_updater_method_params(default_conf, caplog) -> None:
+    instance_strategy_updater = StrategyUpdater()
+    modified_code = instance_strategy_updater.update_code("""
+def confirm_trade_exit(sell_reason: str):
+    nr_orders = trade.nr_of_successful_buys
+    pass
+    """)
+    assert "exit_reason" in modified_code
+    assert "nr_orders = trade.nr_of_successful_entries" in modified_code
+
+
+def test_strategy_updater_dicts(default_conf, caplog) -> None:
+    instance_strategy_updater = StrategyUpdater()
+    modified_code = instance_strategy_updater.update_code("""
+order_time_in_force = {
+    'buy': 'gtc',
+    'sell': 'ioc'
+}
+order_types = {
+    'buy': 'limit',
+    'sell': 'market',
+    'stoploss': 'market',
+    'stoploss_on_exchange': False
+}
+unfilledtimeout = {
+    'buy': 1,
+    'sell': 2
+}
+""")
+
+    assert "'entry': 'gtc'" in modified_code
+    assert "'exit': 'ioc'" in modified_code
+    assert "'entry': 'limit'" in modified_code
+    assert "'exit': 'market'" in modified_code
+    assert "'entry': 1" in modified_code
+    assert "'exit': 2" in modified_code
+
+
+def test_strategy_updater_comparisons(default_conf, caplog) -> None:
+    instance_strategy_updater = StrategyUpdater()
+    modified_code = instance_strategy_updater.update_code("""
+def confirm_trade_exit(sell_reason):
+    if (sell_reason == 'stop_loss'):
+        pass
+""")
+    assert "exit_reason" in modified_code
+    assert "exit_reason == 'stop_loss'" in modified_code
+
+
+def test_strategy_updater_strings(default_conf, caplog) -> None:
+    instance_strategy_updater = StrategyUpdater()
+
+    modified_code = instance_strategy_updater.update_code("""
+sell_reason == 'sell_signal'
+sell_reason == 'force_sell'
+sell_reason == 'emergency_sell'
+""")
+
+    # those tests currently don't work, next in line.
+    assert "exit_signal" in modified_code
+    assert "exit_reason" in modified_code
+    assert "force_exit" in modified_code
+    assert "emergency_exit" in modified_code
+
+
+def test_strategy_updater_comments(default_conf, caplog) -> None:
+    instance_strategy_updater = StrategyUpdater()
+    modified_code = instance_strategy_updater.update_code("""
+# This is the 1st comment
+import talib.abstract as ta
+# This is the 2nd comment
+import freqtrade.vendor.qtpylib.indicators as qtpylib
+
+
+class someStrategy(IStrategy):
+    INTERFACE_VERSION = 2
+    # This is the 3rd comment
+    # This attribute will be overridden if the config file contains "minimal_roi"
+    minimal_roi = {
+        "0": 0.50
+    }
+
+    # This is the 4th comment
+    stoploss = -0.1
+""")
+
+    assert "This is the 1st comment" in modified_code
+    assert "This is the 2nd comment" in modified_code
+    assert "This is the 3rd comment" in modified_code
+    assert "INTERFACE_VERSION = 3" in modified_code
+    # currently still missing:
+    # Webhook terminology, Telegram notification settings, Strategy/Config settings
diff --git a/tests/test_timerange.py b/tests/test_timerange.py
index 06ff1983a..d1c61704f 100644
--- a/tests/test_timerange.py
+++ b/tests/test_timerange.py
@@ -1,7 +1,6 @@
 # pragma pylint: disable=missing-docstring, C0103
 from datetime import datetime, timezone
 
-import arrow
 import pytest
 
 from freqtrade.configuration import TimeRange
@@ -10,6 +9,8 @@ from freqtrade.exceptions import OperationalException
 
 def test_parse_timerange_incorrect():
 
+    timerange = TimeRange.parse_timerange('')
+    assert timerange == TimeRange(None, None, 0, 0)
     timerange = TimeRange.parse_timerange('20100522-')
     assert TimeRange('date', None, 1274486400, 0) == timerange
     assert timerange.timerange_str == '20100522-'
@@ -67,7 +68,7 @@ def test_subtract_start():
 
 
 def test_adjust_start_if_necessary():
-    min_date = arrow.Arrow(2017, 11, 14, 21, 15, 00)
+    min_date = datetime(2017, 11, 14, 21, 15, 00, tzinfo=timezone.utc)
 
     x = TimeRange('date', 'date', 1510694100, 1510780500)
     # Adjust by 20 candles - min_date == startts
diff --git a/tests/test_wallets.py b/tests/test_wallets.py
index 0117f7427..c3ff4ccd0 100644
--- a/tests/test_wallets.py
+++ b/tests/test_wallets.py
@@ -3,16 +3,18 @@ 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 tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_wallet
+from freqtrade.persistence import Trade
+from tests.conftest import EXMS, create_mock_trades, get_patched_freqtradebot, patch_wallet
 
 
 def test_sync_wallet_at_boot(mocker, default_conf):
     default_conf['dry_run'] = False
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_balances=MagicMock(return_value={
             "BNT": {
                 "free": 1.0,
@@ -43,9 +45,9 @@ def test_sync_wallet_at_boot(mocker, default_conf):
     assert freqtrade.wallets._wallets['GAS'].total == 0.260739
     assert freqtrade.wallets.get_free('BNT') == 1.0
     assert 'USDT' in freqtrade.wallets._wallets
-    assert freqtrade.wallets._last_wallet_refresh > 0
+    assert freqtrade.wallets._last_wallet_refresh is not None
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_balances=MagicMock(return_value={
             "BNT": {
                 "free": 1.2,
@@ -87,7 +89,7 @@ def test_sync_wallet_at_boot(mocker, default_conf):
 def test_sync_wallet_missing_data(mocker, default_conf):
     default_conf['dry_run'] = False
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_balances=MagicMock(return_value={
             "BNT": {
                 "free": 1.0,
@@ -136,7 +138,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
                                                  result1, result2, limit_buy_order_open,
                                                  fee, mocker) -> None:
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         fetch_ticker=ticker,
         create_order=MagicMock(return_value=limit_buy_order_open),
         get_fee=fee
@@ -190,7 +192,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
     (1, 15, 10, 10000, None, 0),  # Below min stake and min_stake > stake_available
     (20, 50, 100, 10000, None, 0),  # Below min stake and stake * 1.3 > min_stake
     (1000, None, 1000, 10000, None, 1000),  # No min-stake-amount could be determined
-    (2000, 15, 2000, 3000, 1500, 500),  # Rebuy - resulting in too high stake amount. Adjusting.
+    (2000, 15, 2000, 3000, 1500, 1500),  # Rebuy - resulting in too high stake amount. Adjusting.
 ])
 def test_validate_stake_amount(
     mocker,
@@ -312,7 +314,7 @@ def test_sync_wallet_futures_live(mocker, default_conf):
         }
     ]
     mocker.patch.multiple(
-        'freqtrade.exchange.Exchange',
+        EXMS,
         get_balances=MagicMock(return_value={
             "USDT": {
                 "free": 900,
@@ -330,7 +332,7 @@ def test_sync_wallet_futures_live(mocker, default_conf):
 
     assert 'USDT' in freqtrade.wallets._wallets
     assert 'ETH/USDT:USDT' in freqtrade.wallets._positions
-    assert freqtrade.wallets._last_wallet_refresh > 0
+    assert freqtrade.wallets._last_wallet_refresh is not None
 
     # Remove ETH/USDT:USDT position
     del mock_result[0]
@@ -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
diff --git a/tests/test_worker.py b/tests/test_worker.py
index 88d495e13..79e2f35d4 100644
--- a/tests/test_worker.py
+++ b/tests/test_worker.py
@@ -8,11 +8,11 @@ import time_machine
 from freqtrade.data.dataprovider import DataProvider
 from freqtrade.enums import State
 from freqtrade.worker import Worker
-from tests.conftest import get_patched_worker, log_has, log_has_re
+from tests.conftest import EXMS, get_patched_worker, log_has, log_has_re
 
 
 def test_worker_state(mocker, default_conf, markets) -> None:
-    mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
+    mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets))
     worker = get_patched_worker(mocker, default_conf)
     assert worker.freqtrade.state is State.RUNNING
 
diff --git a/tests/testdata/XRP_ETH-trades.feather b/tests/testdata/XRP_ETH-trades.feather
new file mode 100644
index 000000000..68e1c8467
Binary files /dev/null and b/tests/testdata/XRP_ETH-trades.feather differ
diff --git a/tests/testdata/futures/UNITTEST_USDT-1h-mark.json b/tests/testdata/futures/UNITTEST_USDT_USDT-1h-mark.json
similarity index 100%
rename from tests/testdata/futures/UNITTEST_USDT-1h-mark.json
rename to tests/testdata/futures/UNITTEST_USDT_USDT-1h-mark.json
diff --git a/tests/testdata/futures/XRP_USDT-1h-futures.json b/tests/testdata/futures/XRP_USDT_USDT-1h-futures.json
similarity index 100%
rename from tests/testdata/futures/XRP_USDT-1h-futures.json
rename to tests/testdata/futures/XRP_USDT_USDT-1h-futures.json
diff --git a/tests/testdata/futures/XRP_USDT-1h-futures.json.gz b/tests/testdata/futures/XRP_USDT_USDT-1h-futures.json.gz
similarity index 100%
rename from tests/testdata/futures/XRP_USDT-1h-futures.json.gz
rename to tests/testdata/futures/XRP_USDT_USDT-1h-futures.json.gz
diff --git a/tests/testdata/futures/XRP_USDT-1h-mark.json b/tests/testdata/futures/XRP_USDT_USDT-1h-mark.json
similarity index 100%
rename from tests/testdata/futures/XRP_USDT-1h-mark.json
rename to tests/testdata/futures/XRP_USDT_USDT-1h-mark.json
diff --git a/tests/testdata/futures/XRP_USDT_USDT-5m-futures.json b/tests/testdata/futures/XRP_USDT_USDT-5m-futures.json
new file mode 100644
index 000000000..006bc7508
--- /dev/null
+++ b/tests/testdata/futures/XRP_USDT_USDT-5m-futures.json
@@ -0,0 +1 @@
+[[1636934400000,1.1893,1.1954,1.1891,1.1941,9289043.5],[1636934700000,1.1941,1.1993,1.1934,1.1972,7267451.9000000004],[1636935000000,1.1972,1.1994,1.1958,1.1963,4199874.2999999998],[1636935300000,1.1963,1.199,1.1963,1.198,3834762.5],[1636935600000,1.198,1.2017,1.198,1.2,5362935.4000000004],[1636935900000,1.2,1.2092,1.1975,1.2083,12116690.4000000004],[1636936200000,1.2084,1.2097,1.2061,1.2079,6959499.2000000002],[1636936500000,1.2079,1.2082,1.2034,1.2056,4608466.2000000002],[1636936800000,1.2056,1.2066,1.2038,1.2055,2005115.8999999999],[1636937100000,1.2055,1.2091,1.2043,1.209,3365548.1000000001],[1636937400000,1.209,1.2159,1.2084,1.2157,8496118.9000000004],[1636937700000,1.2157,1.2165,1.2109,1.2119,5183792.7999999998],[1636938000000,1.2118,1.2147,1.2116,1.2123,3034201.2999999998],[1636938300000,1.2122,1.2133,1.2047,1.2068,5546199.0],[1636938600000,1.2069,1.2089,1.2046,1.2083,2961523.7999999998],[1636938900000,1.2084,1.2092,1.2033,1.2054,2755246.6000000001],[1636939200000,1.2053,1.2059,1.2005,1.2012,3574142.2000000002],[1636939500000,1.2013,1.2047,1.2007,1.2038,2085164.5],[1636939800000,1.2037,1.2064,1.2032,1.2053,1930387.6000000001],[1636940100000,1.2052,1.2074,1.2046,1.2071,1563690.5],[1636940400000,1.207,1.208,1.2064,1.2067,1470500.0],[1636940700000,1.2067,1.2085,1.2062,1.2065,1511111.2],[1636941000000,1.2065,1.2119,1.206,1.2116,3196613.7000000002],[1636941300000,1.2115,1.2119,1.2094,1.2101,2474041.2000000002],[1636941600000,1.2102,1.2116,1.2084,1.2089,1966194.3],[1636941900000,1.2089,1.2102,1.2086,1.2088,1051071.8999999999],[1636942200000,1.2089,1.2092,1.2054,1.2067,1718919.5],[1636942500000,1.2066,1.2088,1.2064,1.2064,1276834.8],[1636942800000,1.2064,1.2074,1.2038,1.2039,1389505.0],[1636943100000,1.2039,1.2076,1.2038,1.2064,1828371.2],[1636943400000,1.2065,1.2082,1.2056,1.2082,1072101.5],[1636943700000,1.2082,1.2116,1.2081,1.2115,2157606.0],[1636944000000,1.2116,1.2136,1.2103,1.2129,1364511.6000000001],[1636944300000,1.2129,1.2138,1.209,1.2095,2037732.3999999999],[1636944600000,1.2095,1.2102,1.2081,1.2086,1376262.8999999999],[1636944900000,1.2087,1.2098,1.2081,1.2089,791578.0],[1636945200000,1.2088,1.2099,1.2075,1.2084,949445.9],[1636945500000,1.2084,1.209,1.2055,1.2073,3957253.0],[1636945800000,1.2071,1.2094,1.207,1.209,1755194.3999999999],[1636946100000,1.2091,1.2091,1.2054,1.2065,2235814.1000000001],[1636946400000,1.2064,1.2079,1.2064,1.2076,1314946.5],[1636946700000,1.2076,1.2076,1.2042,1.2058,1554830.8999999999],[1636947000000,1.2058,1.2073,1.2048,1.2053,1266664.8],[1636947300000,1.2053,1.207,1.2047,1.2067,1332565.7],[1636947600000,1.2067,1.2088,1.2062,1.2083,1539348.6000000001],[1636947900000,1.2082,1.2089,1.207,1.2076,1084474.8999999999],[1636948200000,1.2076,1.2082,1.2067,1.2077,573314.1],[1636948500000,1.2077,1.2095,1.2072,1.2086,1612433.1000000001],[1636948800000,1.2087,1.2114,1.2087,1.2106,1800229.5],[1636949100000,1.2106,1.2109,1.2093,1.2105,1069802.8999999999],[1636949400000,1.2105,1.2105,1.2084,1.2097,1522400.1000000001],[1636949700000,1.2097,1.2126,1.2095,1.2122,1581693.3999999999],[1636950000000,1.2121,1.2124,1.211,1.2113,1502370.0],[1636950300000,1.2113,1.2133,1.2112,1.2125,1275382.6000000001],[1636950600000,1.2127,1.2129,1.2108,1.2112,1669161.7],[1636950900000,1.2112,1.2118,1.2103,1.2109,1281564.8999999999],[1636951200000,1.211,1.2128,1.2107,1.212,1004090.0],[1636951500000,1.212,1.2156,1.2111,1.2155,2016559.1000000001],[1636951800000,1.2154,1.2205,1.2145,1.2178,5314030.4000000004],[1636952100000,1.2178,1.2179,1.2142,1.2148,2687145.2000000002],[1636952400000,1.2148,1.2178,1.2148,1.2178,1223842.8],[1636952700000,1.2177,1.2181,1.2147,1.2152,1500519.6000000001],[1636953000000,1.2152,1.2152,1.211,1.2113,2474496.5],[1636953300000,1.2113,1.214,1.2113,1.2125,1248090.0],[1636953600000,1.2125,1.2141,1.2122,1.2139,921049.0],[1636953900000,1.2139,1.2141,1.2118,1.2128,1198193.3999999999],[1636954200000,1.2127,1.2141,1.211,1.2116,1361937.5],[1636954500000,1.2115,1.2121,1.2104,1.2113,1435204.8999999999],[1636954800000,1.2113,1.2114,1.2091,1.2092,1602274.8999999999],[1636955100000,1.2093,1.2102,1.2092,1.2095,802606.0],[1636955400000,1.2095,1.2095,1.2078,1.2083,1139815.6000000001],[1636955700000,1.2083,1.2109,1.2081,1.2097,1182689.6000000001],[1636956000000,1.2097,1.2103,1.2083,1.2093,1121184.0],[1636956300000,1.2092,1.2112,1.2086,1.2105,1252210.1000000001],[1636956600000,1.2106,1.2162,1.2105,1.2158,2003334.0],[1636956900000,1.2158,1.2158,1.2134,1.2145,2058967.3999999999],[1636957200000,1.2144,1.2177,1.2141,1.2174,1881352.6000000001],[1636957500000,1.2174,1.2189,1.217,1.2184,1592213.6000000001],[1636957800000,1.2183,1.2184,1.2143,1.2151,2138606.5],[1636958100000,1.215,1.2159,1.2127,1.2152,1740152.5],[1636958400000,1.2153,1.2157,1.2145,1.2147,837984.7],[1636958700000,1.2146,1.2159,1.214,1.2145,1068054.1000000001],[1636959000000,1.2144,1.2152,1.2129,1.2129,1080074.0],[1636959300000,1.213,1.2156,1.2126,1.2152,1149888.7],[1636959600000,1.2153,1.2214,1.2152,1.2161,6266177.5999999996],[1636959900000,1.2162,1.217,1.2136,1.2142,1969411.0],[1636960200000,1.2141,1.2192,1.2136,1.2187,2066013.8999999999],[1636960500000,1.2187,1.2188,1.2159,1.217,1351267.1000000001],[1636960800000,1.217,1.2178,1.2153,1.2156,1334536.7],[1636961100000,1.2155,1.2158,1.2128,1.2134,1485421.2],[1636961400000,1.2133,1.2148,1.2105,1.2109,1854928.3999999999],[1636961700000,1.211,1.2128,1.2106,1.2121,1148625.1000000001],[1636962000000,1.2121,1.214,1.2106,1.2112,1123669.3999999999],[1636962300000,1.211,1.212,1.2106,1.2118,877979.1],[1636962600000,1.2118,1.2136,1.2112,1.212,1362349.8],[1636962900000,1.2121,1.2121,1.21,1.21,1193089.0],[1636963200000,1.2101,1.2126,1.2092,1.2105,2158603.5],[1636963500000,1.2105,1.2113,1.2048,1.2055,4513172.2000000002],[1636963800000,1.2055,1.2076,1.2048,1.2067,2875763.3999999999],[1636964100000,1.2066,1.2067,1.2005,1.201,4649136.9000000004],[1636964400000,1.201,1.2044,1.2005,1.2035,2734988.8999999999],[1636964700000,1.2034,1.2047,1.2029,1.2038,1696384.0],[1636965000000,1.2039,1.2047,1.2026,1.2044,1652360.6000000001],[1636965300000,1.2043,1.2063,1.2039,1.206,1682635.8],[1636965600000,1.2061,1.2085,1.2056,1.2081,3810080.7000000002],[1636965900000,1.2081,1.2109,1.2069,1.21,3426972.6000000001],[1636966200000,1.21,1.2108,1.2094,1.2099,1948298.3999999999],[1636966500000,1.2099,1.2108,1.2088,1.2105,1358895.3999999999],[1636966800000,1.2104,1.215,1.2089,1.215,5488008.9000000004],[1636967100000,1.2149,1.2175,1.2145,1.2156,4423040.2000000002],[1636967400000,1.2156,1.2199,1.2156,1.2193,3209461.7000000002],[1636967700000,1.2193,1.22,1.2161,1.2177,3136263.2999999998],[1636968000000,1.2177,1.2178,1.2144,1.2154,2973911.8999999999],[1636968300000,1.2155,1.2185,1.2151,1.2161,2313273.1000000001],[1636968600000,1.2161,1.2161,1.2118,1.2138,2457956.6000000001],[1636968900000,1.2139,1.2147,1.2111,1.212,1467335.5],[1636969200000,1.212,1.2121,1.2102,1.2102,1479763.0],[1636969500000,1.2102,1.2124,1.2101,1.2116,1759979.3],[1636969800000,1.2115,1.2126,1.2114,1.2125,597927.7],[1636970100000,1.2123,1.2127,1.2109,1.2113,846797.8],[1636970400000,1.2113,1.2116,1.2054,1.206,4003530.2999999998],[1636970700000,1.2061,1.2085,1.2061,1.2083,1407880.3],[1636971000000,1.2084,1.2092,1.2071,1.209,686922.7],[1636971300000,1.209,1.2098,1.2089,1.2094,969569.7],[1636971600000,1.2095,1.2101,1.2085,1.2101,958910.5],[1636971900000,1.2101,1.2107,1.206,1.2066,1750771.2],[1636972200000,1.2065,1.2091,1.2065,1.2085,863380.4],[1636972500000,1.2085,1.2112,1.2085,1.211,885808.6],[1636972800000,1.2111,1.2111,1.2091,1.2093,768551.4],[1636973100000,1.2096,1.21,1.2086,1.2088,596104.7],[1636973400000,1.2088,1.2116,1.2088,1.2102,1013463.8],[1636973700000,1.2102,1.2108,1.2089,1.21,868464.8],[1636974000000,1.21,1.2105,1.2084,1.2088,1146027.1000000001],[1636974300000,1.2089,1.2106,1.2088,1.2105,775161.5],[1636974600000,1.2106,1.2106,1.2093,1.2093,549029.5],[1636974900000,1.2094,1.2097,1.2079,1.2085,616718.4],[1636975200000,1.2085,1.2089,1.2049,1.2064,4021848.5],[1636975500000,1.2063,1.2076,1.205,1.2056,1516604.6000000001],[1636975800000,1.2056,1.2056,1.2027,1.2037,3276909.2000000002],[1636976100000,1.2038,1.2058,1.2032,1.2058,1671933.3],[1636976400000,1.2058,1.2075,1.2058,1.2068,1309347.1000000001],[1636976700000,1.2068,1.2069,1.2057,1.2063,945417.0],[1636977000000,1.2063,1.2077,1.2059,1.2071,919673.8],[1636977300000,1.2072,1.2074,1.2062,1.2073,742663.2],[1636977600000,1.2073,1.209,1.205,1.2075,2478967.0],[1636977900000,1.2075,1.2077,1.204,1.2054,1870332.3],[1636978200000,1.2055,1.2059,1.2049,1.2053,565997.4],[1636978500000,1.2052,1.2053,1.2015,1.2037,2654152.1000000001],[1636978800000,1.2036,1.2047,1.2022,1.2038,1807444.1000000001],[1636979100000,1.2038,1.2046,1.2026,1.2027,1065979.8],[1636979400000,1.2027,1.206,1.2027,1.2054,1477203.8],[1636979700000,1.2053,1.2067,1.204,1.2045,2211979.5],[1636980000000,1.2047,1.2059,1.204,1.2058,804279.6],[1636980300000,1.2058,1.2066,1.205,1.2066,1313472.0],[1636980600000,1.2066,1.2067,1.2055,1.2066,834835.7],[1636980900000,1.2067,1.2073,1.2047,1.2047,1000096.8],[1636981200000,1.2047,1.207,1.2046,1.2068,1046862.8],[1636981500000,1.2068,1.2077,1.2066,1.207,1188865.6000000001],[1636981800000,1.2071,1.2104,1.207,1.2101,1875017.0],[1636982100000,1.2102,1.2114,1.2069,1.2072,2439900.5],[1636982400000,1.2072,1.2084,1.2066,1.2078,865686.6],[1636982700000,1.2078,1.2079,1.2064,1.2066,633128.7],[1636983000000,1.2065,1.2087,1.202,1.202,5720475.0999999996],[1636983300000,1.2021,1.2038,1.2005,1.2038,3417445.1000000001],[1636983600000,1.2038,1.2047,1.1915,1.1991,14721245.0999999996],[1636983900000,1.1992,1.1995,1.1953,1.1974,5430137.5],[1636984200000,1.1974,1.1996,1.1973,1.1983,1598326.5],[1636984500000,1.1983,1.1996,1.1979,1.1989,1487378.3999999999],[1636984800000,1.1989,1.1991,1.1941,1.195,3172429.0],[1636985100000,1.195,1.1965,1.1941,1.1964,3403294.0],[1636985400000,1.1965,1.1977,1.1934,1.1945,2772952.6000000001],[1636985700000,1.1944,1.1965,1.1924,1.1928,2853416.8999999999],[1636986000000,1.1927,1.1944,1.1864,1.1881,9859645.0999999996],[1636986300000,1.1881,1.1934,1.1865,1.192,4374940.7000000002],[1636986600000,1.1921,1.1931,1.1904,1.1927,2201904.7000000002],[1636986900000,1.1926,1.1944,1.1924,1.1928,1807342.7],[1636987200000,1.193,1.1941,1.1925,1.193,1354005.2],[1636987500000,1.193,1.193,1.1899,1.1906,2118339.0],[1636987800000,1.1905,1.1918,1.1879,1.1905,4748915.5],[1636988100000,1.1906,1.1927,1.1902,1.191,1886338.7],[1636988400000,1.1911,1.1916,1.1866,1.1866,4544292.0999999996],[1636988700000,1.1866,1.1887,1.1844,1.1871,7310980.2000000002],[1636989000000,1.1872,1.1914,1.1862,1.1908,3315857.5],[1636989300000,1.1908,1.1922,1.189,1.1914,2196378.0],[1636989600000,1.1913,1.1913,1.1895,1.1909,1199510.5],[1636989900000,1.1908,1.1927,1.189,1.1899,1838353.0],[1636990200000,1.1899,1.1923,1.1885,1.1917,1453015.3],[1636990500000,1.1915,1.192,1.1883,1.1893,1395222.3999999999],[1636990800000,1.1893,1.1893,1.186,1.1862,2115580.1000000001],[1636991100000,1.1862,1.1909,1.1854,1.1907,2681118.7000000002],[1636991400000,1.1907,1.1908,1.186,1.1875,1802497.2],[1636991700000,1.1875,1.1896,1.1873,1.1885,1338971.5],[1636992000000,1.1885,1.19,1.1855,1.19,4843721.2000000002],[1636992300000,1.19,1.1919,1.1888,1.1892,2129650.7000000002],[1636992600000,1.1892,1.1904,1.1865,1.187,2149370.6000000001],[1636992900000,1.1871,1.1896,1.1861,1.1872,1778567.1000000001],[1636993200000,1.1871,1.1875,1.1862,1.1864,1184527.7],[1636993500000,1.1864,1.1873,1.1821,1.185,5233565.7999999998],[1636993800000,1.185,1.1894,1.1836,1.1894,2780444.8999999999],[1636994100000,1.1894,1.1913,1.1879,1.1881,1948790.6000000001],[1636994400000,1.1881,1.1893,1.1844,1.1847,2805736.3999999999],[1636994700000,1.1847,1.1857,1.1816,1.1821,3092493.8999999999],[1636995000000,1.1822,1.1849,1.1811,1.1849,2919570.1000000001],[1636995300000,1.1848,1.1897,1.1845,1.1897,2529541.7000000002],[1636995600000,1.1897,1.1898,1.1865,1.1881,1987137.8999999999],[1636995900000,1.188,1.1883,1.1864,1.1882,1229479.8999999999],[1636996200000,1.1882,1.1904,1.188,1.1894,2329656.5],[1636996500000,1.1894,1.1907,1.1877,1.1885,1635524.7],[1636996800000,1.1885,1.1885,1.1861,1.1866,1464212.6000000001],[1636997100000,1.1866,1.1881,1.1859,1.1872,1498541.1000000001],[1636997400000,1.1872,1.1884,1.1866,1.1877,645062.1],[1636997700000,1.1878,1.189,1.1877,1.1887,976463.5],[1636998000000,1.1886,1.1903,1.1884,1.1894,936264.1],[1636998300000,1.1893,1.1902,1.188,1.1881,1076340.3],[1636998600000,1.1881,1.1884,1.1855,1.1877,1660911.8],[1636998900000,1.1877,1.1877,1.1847,1.1848,1546267.1000000001],[1636999200000,1.1848,1.1857,1.1831,1.1843,3223795.7000000002],[1636999500000,1.1842,1.1878,1.184,1.1873,1328197.3999999999],[1636999800000,1.1873,1.1885,1.1849,1.1851,1479273.8999999999],[1637000100000,1.1851,1.1858,1.1826,1.1847,2957345.1000000001],[1637000400000,1.1846,1.1875,1.1841,1.1863,1262899.8999999999],[1637000700000,1.1863,1.1866,1.1846,1.1855,1498078.8],[1637001000000,1.1854,1.186,1.1845,1.1847,907889.9],[1637001300000,1.1847,1.186,1.1846,1.1848,681753.2],[1637001600000,1.1847,1.1852,1.1814,1.1825,1997175.0],[1637001900000,1.1824,1.1826,1.18,1.1804,4002404.7999999998],[1637002200000,1.1804,1.1812,1.1756,1.1811,6730780.9000000004],[1637002500000,1.1811,1.1836,1.1805,1.1825,2072583.3],[1637002800000,1.1825,1.1826,1.1785,1.181,2788627.7999999998],[1637003100000,1.181,1.1817,1.1791,1.1808,1166716.5],[1637003400000,1.1808,1.1835,1.1806,1.1835,1356426.8999999999],[1637003700000,1.1835,1.1839,1.1826,1.1831,791629.1],[1637004000000,1.1831,1.1834,1.1806,1.1833,1158697.8],[1637004300000,1.1833,1.1863,1.1831,1.1853,2155591.2000000002],[1637004600000,1.1854,1.1879,1.1843,1.1878,922252.4],[1637004900000,1.1879,1.1879,1.1847,1.1851,1403022.8],[1637005200000,1.185,1.1875,1.1847,1.1865,1031169.4],[1637005500000,1.1865,1.1874,1.1848,1.1852,662437.6],[1637005800000,1.1853,1.1855,1.184,1.1846,751629.5],[1637006100000,1.1847,1.1859,1.1842,1.1857,949722.6],[1637006400000,1.1857,1.1857,1.1806,1.1816,1733988.6000000001],[1637006700000,1.1815,1.1816,1.1784,1.1796,2888343.2000000002],[1637007000000,1.1796,1.1816,1.1785,1.1808,1077627.7],[1637007300000,1.1807,1.1807,1.178,1.1785,1101824.3999999999],[1637007600000,1.1785,1.1788,1.1764,1.1777,2142094.7000000002],[1637007900000,1.1776,1.1783,1.1759,1.1759,2095915.1000000001],[1637008200000,1.1758,1.1796,1.1755,1.1789,2472558.2000000002],[1637008500000,1.1789,1.1804,1.1764,1.1765,1733359.2],[1637008800000,1.1764,1.1788,1.1735,1.1778,4928727.0],[1637009100000,1.1778,1.1809,1.1766,1.1804,1891969.1000000001],[1637009400000,1.1804,1.1826,1.1793,1.1797,1792413.3999999999],[1637009700000,1.1796,1.1803,1.1768,1.1777,1422290.8],[1637010000000,1.1776,1.1776,1.165,1.1737,15867137.6999999993],[1637010300000,1.1737,1.1766,1.1721,1.1729,2896715.1000000001],[1637010600000,1.1729,1.1765,1.1726,1.1759,1538151.0],[1637010900000,1.1759,1.179,1.1758,1.1784,2383917.6000000001],[1637011200000,1.1785,1.1794,1.1768,1.1777,1266720.3],[1637011500000,1.1776,1.1799,1.1749,1.1772,2843606.2999999998],[1637011800000,1.1773,1.1803,1.1758,1.1801,1386825.8999999999],[1637012100000,1.1801,1.1802,1.1771,1.1783,1025132.1],[1637012400000,1.1781,1.1799,1.1771,1.1798,1095931.5],[1637012700000,1.1797,1.1807,1.1788,1.179,824766.1],[1637013000000,1.179,1.1802,1.178,1.1788,736602.1],[1637013300000,1.1789,1.1827,1.1789,1.1805,1921139.5],[1637013600000,1.1805,1.1807,1.177,1.1775,1388575.0],[1637013900000,1.1774,1.1786,1.1772,1.1777,851274.7],[1637014200000,1.1778,1.1781,1.1754,1.1756,1229630.5],[1637014500000,1.1755,1.1756,1.1725,1.1736,2284743.7000000002],[1637014800000,1.1735,1.1737,1.1701,1.1728,2143691.1000000001],[1637015100000,1.1728,1.1732,1.1715,1.1719,1255188.1000000001],[1637015400000,1.1719,1.1756,1.1719,1.1755,1242001.8],[1637015700000,1.1756,1.1773,1.1754,1.176,990468.2],[1637016000000,1.176,1.1777,1.1759,1.1775,498149.6],[1637016300000,1.1776,1.1793,1.1772,1.1775,750568.6],[1637016600000,1.1775,1.1804,1.1772,1.1803,941858.4],[1637016900000,1.1804,1.1824,1.179,1.1812,2075547.3],[1637017200000,1.1812,1.1812,1.1772,1.1778,1938884.0],[1637017500000,1.1777,1.1792,1.1774,1.1782,799165.2],[1637017800000,1.1782,1.1789,1.1777,1.1777,478035.0],[1637018100000,1.1778,1.1785,1.174,1.1782,4335357.7999999998],[1637018400000,1.1783,1.1784,1.1765,1.1767,697003.9],[1637018700000,1.1767,1.1768,1.1729,1.1734,1483791.0],[1637019000000,1.1733,1.1761,1.1726,1.1751,1632155.0],[1637019300000,1.1752,1.1763,1.1751,1.1756,618483.1],[1637019600000,1.1758,1.1767,1.1748,1.1764,551544.6],[1637019900000,1.1765,1.1767,1.1755,1.1761,769943.5],[1637020200000,1.1761,1.1766,1.175,1.1758,815763.7],[1637020500000,1.1758,1.1764,1.1707,1.1729,2601489.3999999999],[1637020800000,1.1728,1.1729,1.1626,1.1647,10370232.6999999993],[1637021100000,1.1647,1.1679,1.1574,1.1588,14674219.0999999996],[1637021400000,1.1588,1.1603,1.125,1.1505,51767444.6000000015],[1637021700000,1.1506,1.1562,1.1447,1.1485,12232299.4000000004],[1637022000000,1.1484,1.1527,1.1436,1.1519,6240786.5999999996],[1637022300000,1.152,1.1526,1.1458,1.1493,5898863.9000000004],[1637022600000,1.1493,1.1543,1.1453,1.1542,4023262.8999999999],[1637022900000,1.1542,1.1548,1.1503,1.1539,4072291.5],[1637023200000,1.1539,1.1548,1.1513,1.1526,2831077.2999999998],[1637023500000,1.1525,1.1573,1.1523,1.1525,3310198.0],[1637023800000,1.1524,1.1536,1.1482,1.1488,3801330.7000000002],[1637024100000,1.1486,1.1514,1.142,1.1432,8989381.1999999993],[1637024400000,1.1432,1.1472,1.13,1.1324,18542491.1999999993],[1637024700000,1.1322,1.139,1.105,1.137,49788431.8999999985],[1637025000000,1.1371,1.1391,1.1308,1.1388,16654546.9000000004],[1637025300000,1.1387,1.1412,1.1307,1.1356,14918494.0999999996],[1637025600000,1.1356,1.1414,1.1314,1.138,7098702.4000000004],[1637025900000,1.138,1.1416,1.1364,1.1393,5430829.7000000002],[1637026200000,1.1393,1.1401,1.13,1.1307,6803088.2000000002],[1637026500000,1.1307,1.1365,1.1261,1.1363,6164932.5],[1637026800000,1.1363,1.1459,1.136,1.1454,4365617.4000000004],[1637027100000,1.1453,1.1461,1.1419,1.145,4744308.5],[1637027400000,1.145,1.1471,1.1429,1.1453,3721814.3999999999],[1637027700000,1.1453,1.1458,1.1416,1.1424,2823682.3999999999],[1637028000000,1.1425,1.1438,1.1387,1.1402,3710527.2999999998],[1637028300000,1.1401,1.1418,1.1383,1.1411,3186377.5],[1637028600000,1.1411,1.1443,1.1405,1.143,3430024.2999999998],[1637028900000,1.1431,1.1437,1.1414,1.1429,2317514.3999999999],[1637029200000,1.1429,1.1435,1.1411,1.1429,2652508.5],[1637029500000,1.1429,1.1437,1.1415,1.1415,1811976.5],[1637029800000,1.1416,1.1422,1.14,1.1404,1639570.8],[1637030100000,1.1404,1.141,1.135,1.1374,5626771.4000000004],[1637030400000,1.1374,1.1388,1.1361,1.1367,1883461.3999999999],[1637030700000,1.1366,1.1396,1.1345,1.1354,2704401.7999999998],[1637031000000,1.1353,1.1368,1.1314,1.1314,3254363.8999999999],[1637031300000,1.1315,1.1319,1.1274,1.1302,6785507.5999999996],[1637031600000,1.1302,1.1303,1.1237,1.1302,7233618.5],[1637031900000,1.1303,1.1343,1.1296,1.1324,4118353.2999999998],[1637032200000,1.1324,1.1349,1.1321,1.1343,1859638.2],[1637032500000,1.1343,1.1351,1.1326,1.1345,1874930.3999999999],[1637032800000,1.1344,1.1354,1.1245,1.1268,6000321.0],[1637033100000,1.1268,1.1284,1.1121,1.1191,12805164.5999999996],[1637033400000,1.1192,1.1258,1.1164,1.1182,8744035.5999999996],[1637033700000,1.1182,1.1231,1.111,1.118,9232101.1999999993],[1637034000000,1.118,1.1239,1.1163,1.1165,6494642.2000000002],[1637034300000,1.1166,1.1232,1.1111,1.123,8552285.3000000007],[1637034600000,1.123,1.1286,1.121,1.1268,7567469.7999999998],[1637034900000,1.1268,1.1289,1.1211,1.1228,4671142.5999999996],[1637035200000,1.1228,1.1254,1.1159,1.1175,9853542.0],[1637035500000,1.1174,1.1239,1.1148,1.1238,6547575.7000000002],[1637035800000,1.1238,1.124,1.1122,1.1137,7136291.2000000002],[1637036100000,1.1138,1.1188,1.112,1.1128,6459000.0999999996],[1637036400000,1.1127,1.1195,1.1061,1.1195,15800732.3000000007],[1637036700000,1.1195,1.1289,1.1181,1.1273,7311133.7999999998],[1637037000000,1.1272,1.1299,1.1249,1.1287,6075163.2000000002],[1637037300000,1.1287,1.1318,1.1276,1.1309,3822050.1000000001],[1637037600000,1.1309,1.1324,1.1277,1.1314,4134442.7000000002],[1637037900000,1.1314,1.1314,1.1258,1.1289,3024553.7999999998],[1637038200000,1.1288,1.1328,1.1281,1.1324,3120336.0],[1637038500000,1.1323,1.1324,1.1286,1.1286,3527058.7999999998],[1637038800000,1.1285,1.1303,1.1275,1.1284,2208750.0],[1637039100000,1.1284,1.1285,1.1239,1.1283,2784313.2999999998],[1637039400000,1.1282,1.1284,1.1244,1.1249,2212214.7000000002],[1637039700000,1.125,1.1299,1.1248,1.1294,2706724.0],[1637040000000,1.1294,1.1326,1.1285,1.1323,2981264.8999999999],[1637040300000,1.1322,1.1327,1.1254,1.1289,5296197.7000000002],[1637040600000,1.1289,1.1312,1.1283,1.129,2207684.8999999999],[1637040900000,1.1289,1.129,1.1262,1.1262,1581778.5],[1637041200000,1.1264,1.1312,1.1228,1.1303,6245071.4000000004],[1637041500000,1.1302,1.1303,1.1264,1.1268,1430621.3999999999],[1637041800000,1.1268,1.1273,1.1242,1.1261,2663380.2000000002],[1637042100000,1.1262,1.1262,1.1216,1.1234,3088115.1000000001],[1637042400000,1.1235,1.1258,1.1215,1.1257,2607308.1000000001],[1637042700000,1.1257,1.1265,1.1204,1.1212,2930686.7999999998],[1637043000000,1.1213,1.1253,1.1212,1.125,1715592.8],[1637043300000,1.1249,1.1251,1.1213,1.1214,2267349.7000000002],[1637043600000,1.1213,1.1236,1.1208,1.1222,1124855.1000000001],[1637043900000,1.1223,1.1238,1.1155,1.1164,5530558.7999999998],[1637044200000,1.1166,1.1224,1.115,1.1224,5299299.0],[1637044500000,1.1224,1.1239,1.1186,1.1191,3826527.0],[1637044800000,1.1191,1.1214,1.1174,1.1199,2721175.3999999999],[1637045100000,1.1198,1.1244,1.1196,1.1226,3202701.1000000001],[1637045400000,1.1226,1.125,1.1223,1.1235,1864276.0],[1637045700000,1.1236,1.1267,1.122,1.1248,2202205.6000000001],[1637046000000,1.1247,1.1264,1.1197,1.12,3604061.2000000002],[1637046300000,1.1201,1.1277,1.12,1.127,3024670.2999999998],[1637046600000,1.127,1.127,1.1221,1.1228,2208623.7000000002],[1637046900000,1.1227,1.1266,1.1224,1.1228,2422007.5],[1637047200000,1.1228,1.1228,1.1198,1.1202,1741483.3],[1637047500000,1.1201,1.124,1.1197,1.1226,2176612.3999999999],[1637047800000,1.1226,1.1268,1.1208,1.1213,2963321.1000000001],[1637048100000,1.1215,1.1216,1.1187,1.1197,2621660.7999999998],[1637048400000,1.1198,1.122,1.1188,1.1217,1715248.7],[1637048700000,1.1216,1.1238,1.121,1.1237,1076378.6000000001],[1637049000000,1.1237,1.126,1.119,1.1242,2665782.8999999999],[1637049300000,1.1243,1.1258,1.1226,1.1255,2099191.7000000002],[1637049600000,1.1256,1.1274,1.1218,1.1258,4283096.0],[1637049900000,1.1259,1.1293,1.1257,1.1289,3450819.5],[1637050200000,1.129,1.1297,1.1247,1.1263,2658729.6000000001],[1637050500000,1.1263,1.1265,1.1216,1.1248,2353989.0],[1637050800000,1.1248,1.128,1.1235,1.1254,2667401.7999999998],[1637051100000,1.1255,1.1257,1.1224,1.1231,1663270.7],[1637051400000,1.1232,1.126,1.1228,1.1257,1504233.5],[1637051700000,1.1257,1.126,1.1235,1.1249,1636380.3],[1637052000000,1.1249,1.1287,1.1248,1.1286,2152550.8999999999],[1637052300000,1.1287,1.1291,1.1258,1.1258,1642710.6000000001],[1637052600000,1.1258,1.1299,1.1234,1.1295,2840899.0],[1637052900000,1.1294,1.1312,1.1292,1.1301,2753850.0],[1637053200000,1.13,1.1347,1.1275,1.1281,6623462.7000000002],[1637053500000,1.1282,1.1282,1.125,1.1252,3362681.8999999999],[1637053800000,1.1252,1.1281,1.1246,1.1264,1587342.8],[1637054100000,1.1264,1.1264,1.1209,1.121,2291705.7000000002],[1637054400000,1.121,1.1231,1.119,1.1206,4287524.7999999998],[1637054700000,1.1205,1.1211,1.1183,1.121,2149620.3999999999],[1637055000000,1.121,1.1211,1.1177,1.1198,1889836.2],[1637055300000,1.1198,1.1198,1.1109,1.1127,6507103.7000000002],[1637055600000,1.1127,1.1172,1.1125,1.1138,3022620.7999999998],[1637055900000,1.1138,1.1156,1.1101,1.1102,2856203.2000000002],[1637056200000,1.1101,1.1141,1.103,1.1124,13114838.6999999993],[1637056500000,1.1124,1.113,1.1027,1.1032,6424556.0999999996],[1637056800000,1.1031,1.1051,1.08,1.0959,39145521.200000003],[1637057100000,1.0959,1.0959,1.0392,1.0535,56581244.5],[1637057400000,1.0546,1.0729,1.0332,1.0439,42637203.3999999985],[1637057700000,1.0439,1.0661,1.0347,1.0657,38320115.5],[1637058000000,1.0658,1.0692,1.0616,1.064,17025610.8000000007],[1637058300000,1.0641,1.0721,1.057,1.0714,17492906.8000000007],[1637058600000,1.0714,1.0828,1.0676,1.0819,15396169.6999999993],[1637058900000,1.0818,1.089,1.0782,1.0821,15832077.6999999993],[1637059200000,1.0821,1.0877,1.08,1.0869,10945208.6999999993],[1637059500000,1.087,1.0871,1.0819,1.083,6231017.7000000002],[1637059800000,1.083,1.0868,1.0804,1.0858,5392462.9000000004],[1637060100000,1.0856,1.0947,1.0844,1.0927,8293781.2999999998],[1637060400000,1.0927,1.0991,1.0889,1.0948,14201629.4000000004],[1637060700000,1.0948,1.0956,1.092,1.0927,10489689.5999999996],[1637061000000,1.0928,1.0932,1.0893,1.092,5223379.2000000002],[1637061300000,1.092,1.0933,1.0892,1.0917,3130525.8999999999],[1637061600000,1.0917,1.0951,1.091,1.0924,4478758.0],[1637061900000,1.0922,1.0961,1.09,1.0961,4269518.2000000002],[1637062200000,1.0961,1.0965,1.0912,1.0944,4219889.2000000002],[1637062500000,1.0944,1.0963,1.092,1.0933,4445458.7999999998],[1637062800000,1.0932,1.0942,1.0893,1.0903,4145158.7000000002],[1637063100000,1.0904,1.0929,1.0898,1.0915,2970968.0],[1637063400000,1.0915,1.094,1.081,1.0869,14647309.9000000004],[1637063700000,1.0869,1.0912,1.0849,1.091,5089475.5999999996],[1637064000000,1.091,1.091,1.0854,1.0869,6876454.0999999996],[1637064300000,1.087,1.0936,1.0847,1.0915,6451835.0999999996],[1637064600000,1.0915,1.0936,1.0883,1.0935,5260521.0],[1637064900000,1.0936,1.0953,1.0917,1.0936,3561178.1000000001],[1637065200000,1.0936,1.0947,1.0919,1.092,5160451.7999999998],[1637065500000,1.092,1.0933,1.0875,1.0883,7458006.2999999998],[1637065800000,1.0883,1.0916,1.0864,1.0912,4436506.2000000002],[1637066100000,1.0912,1.0913,1.0865,1.0883,3664288.7999999998],[1637066400000,1.0883,1.0904,1.0876,1.0895,2131346.2000000002],[1637066700000,1.0896,1.0911,1.088,1.0891,1823000.0],[1637067000000,1.0891,1.0892,1.0858,1.0869,2434249.6000000001],[1637067300000,1.0869,1.0874,1.0781,1.0807,10353451.5999999996],[1637067600000,1.0807,1.0891,1.0807,1.0872,7115227.4000000004],[1637067900000,1.0872,1.0875,1.0831,1.0861,2835656.5],[1637068200000,1.0861,1.0871,1.0837,1.0862,1842530.0],[1637068500000,1.0863,1.0931,1.0861,1.0919,4535343.2000000002],[1637068800000,1.0919,1.0919,1.0896,1.0908,1767315.6000000001],[1637069100000,1.0909,1.0965,1.0908,1.0925,4127187.2999999998],[1637069400000,1.0925,1.0931,1.0874,1.0889,2829371.3999999999],[1637069700000,1.089,1.0941,1.0889,1.0933,2691450.5],[1637070000000,1.0933,1.0982,1.0933,1.0975,3449425.3999999999],[1637070300000,1.0976,1.0995,1.0949,1.0956,3111204.2000000002],[1637070600000,1.0955,1.0977,1.0922,1.097,3361311.6000000001],[1637070900000,1.0971,1.0978,1.0953,1.0955,1874257.1000000001],[1637071200000,1.0955,1.1002,1.0939,1.0996,2874238.1000000001],[1637071500000,1.0995,1.1072,1.0988,1.106,5584774.5999999996],[1637071800000,1.106,1.1063,1.1006,1.1021,6260439.5],[1637072100000,1.1021,1.1053,1.1007,1.1035,2629958.5],[1637072400000,1.1036,1.1071,1.1035,1.1063,2563542.7000000002],[1637072700000,1.1064,1.1074,1.1034,1.1058,2746573.6000000001],[1637073000000,1.1058,1.1088,1.104,1.1067,3563226.6000000001],[1637073300000,1.1067,1.1145,1.1067,1.1124,6518384.7999999998],[1637073600000,1.1125,1.1148,1.1122,1.1147,3836367.8999999999],[1637073900000,1.1147,1.1166,1.1131,1.1139,6381942.5999999996],[1637074200000,1.1138,1.1145,1.1099,1.1105,4349913.0999999996],[1637074500000,1.1105,1.1107,1.1071,1.1101,3994190.7999999998],[1637074800000,1.1101,1.1135,1.1076,1.1091,3753506.8999999999],[1637075100000,1.1091,1.1134,1.1089,1.1113,3603867.3999999999],[1637075400000,1.1112,1.1113,1.107,1.1083,4664129.0],[1637075700000,1.1083,1.1103,1.1064,1.1094,2488059.8999999999],[1637076000000,1.1093,1.1099,1.1039,1.1058,3100351.5],[1637076300000,1.1057,1.1066,1.1051,1.1065,1414436.3999999999],[1637076600000,1.1065,1.1078,1.1012,1.1028,3693132.0],[1637076900000,1.1027,1.1053,1.1026,1.1032,1753110.5],[1637077200000,1.1033,1.1041,1.1006,1.1018,1495171.1000000001],[1637077500000,1.1019,1.1028,1.0997,1.1002,2868409.6000000001],[1637077800000,1.1002,1.102,1.0994,1.1014,1944395.6000000001],[1637078100000,1.1014,1.1019,1.0958,1.0994,4411719.7000000002],[1637078400000,1.0995,1.1042,1.0993,1.1011,4274375.2000000002],[1637078700000,1.1011,1.1019,1.0976,1.1016,4437800.0],[1637079000000,1.1015,1.1062,1.101,1.1043,3558900.2999999998],[1637079300000,1.1042,1.1084,1.1042,1.1047,3116961.7000000002],[1637079600000,1.1047,1.1064,1.1028,1.1032,2383209.5],[1637079900000,1.1031,1.107,1.1019,1.1059,3174573.7000000002],[1637080200000,1.1059,1.1084,1.1052,1.1053,2027170.3999999999],[1637080500000,1.1054,1.1071,1.1028,1.1041,2097532.7999999998],[1637080800000,1.1039,1.1042,1.1009,1.1022,2384703.5],[1637081100000,1.1022,1.1042,1.1021,1.103,1090303.7],[1637081400000,1.1029,1.106,1.1026,1.1053,1679083.3],[1637081700000,1.1054,1.1062,1.1044,1.1047,1281072.5],[1637082000000,1.1047,1.1082,1.1046,1.1075,1870676.6000000001],[1637082300000,1.1075,1.1084,1.1055,1.106,1514063.3999999999],[1637082600000,1.1061,1.1074,1.1059,1.1066,1127931.8],[1637082900000,1.1067,1.1077,1.1052,1.106,2205945.3999999999],[1637083200000,1.106,1.106,1.1014,1.1032,2175136.6000000001],[1637083500000,1.1033,1.104,1.1026,1.1035,681357.6],[1637083800000,1.1035,1.1036,1.0965,1.0987,5090985.2999999998],[1637084100000,1.0988,1.1014,1.0985,1.1007,1185052.1000000001],[1637084400000,1.1007,1.1012,1.0986,1.1011,1336553.0],[1637084700000,1.1011,1.1022,1.0995,1.101,1619343.1000000001],[1637085000000,1.1011,1.1027,1.0995,1.1019,1152992.1000000001],[1637085300000,1.102,1.1024,1.0986,1.0988,1475514.2],[1637085600000,1.0988,1.1033,1.0987,1.1029,1140507.3],[1637085900000,1.1029,1.1055,1.1021,1.1028,2751272.6000000001],[1637086200000,1.1028,1.1034,1.1021,1.1029,870292.5],[1637086500000,1.1029,1.1039,1.1009,1.1014,856580.6],[1637086800000,1.1014,1.1027,1.1014,1.1018,616637.1],[1637087100000,1.1017,1.1019,1.0975,1.0979,1535581.5],[1637087400000,1.0979,1.1009,1.0977,1.0977,1669346.1000000001],[1637087700000,1.0978,1.1001,1.0924,1.0928,4071053.2999999998],[1637088000000,1.0928,1.0945,1.0886,1.0913,5158080.7000000002],[1637088300000,1.0912,1.0916,1.0875,1.0888,3913693.2000000002],[1637088600000,1.0888,1.092,1.0883,1.091,2824819.2000000002],[1637088900000,1.091,1.0922,1.0863,1.0863,1971506.7],[1637089200000,1.0862,1.0964,1.0849,1.095,5786081.9000000004],[1637089500000,1.0949,1.0994,1.0948,1.0988,2539197.7000000002],[1637089800000,1.0987,1.0998,1.0953,1.0959,2441709.2000000002],[1637090100000,1.096,1.0985,1.0931,1.0984,2598843.5],[1637090400000,1.0984,1.0995,1.0975,1.0986,1570715.7],[1637090700000,1.0985,1.0988,1.0956,1.098,1618041.8],[1637091000000,1.098,1.1013,1.0964,1.0985,2150798.7999999998],[1637091300000,1.0983,1.0998,1.0923,1.0933,3299815.6000000001],[1637091600000,1.0934,1.0947,1.093,1.093,1482874.0],[1637091900000,1.0931,1.0936,1.0814,1.0827,9204218.9000000004],[1637092200000,1.0827,1.0865,1.0817,1.0836,3364077.2000000002],[1637092500000,1.0836,1.0932,1.0835,1.0922,3895856.6000000001],[1637092800000,1.0922,1.0922,1.0854,1.0884,3260526.3999999999],[1637093100000,1.0883,1.0927,1.0883,1.089,2290681.6000000001],[1637093400000,1.0889,1.0906,1.0846,1.0864,3570163.0],[1637093700000,1.0863,1.0866,1.0805,1.0841,5326198.9000000004],[1637094000000,1.0841,1.0884,1.0835,1.0853,2398866.2999999998],[1637094300000,1.0852,1.0889,1.0809,1.0812,5647177.2999999998],[1637094600000,1.0812,1.0837,1.0764,1.0807,8712026.0999999996],[1637094900000,1.0808,1.0821,1.0705,1.0711,8140649.7999999998],[1637095200000,1.071,1.0758,1.0686,1.0717,8179432.7000000002],[1637095500000,1.0719,1.0783,1.0656,1.0768,8720833.5999999996],[1637095800000,1.0768,1.079,1.0711,1.0721,6371077.5],[1637096100000,1.0721,1.0811,1.0721,1.0807,6004000.5999999996],[1637096400000,1.0806,1.0901,1.0793,1.09,5236339.0],[1637096700000,1.09,1.0942,1.088,1.0939,7370866.0],[1637097000000,1.0939,1.0949,1.0906,1.0946,4293189.4000000004],[1637097300000,1.0944,1.0978,1.0933,1.0939,4202180.5999999996],[1637097600000,1.0938,1.0972,1.0922,1.0957,2739087.2000000002],[1637097900000,1.0958,1.096,1.0911,1.0925,2403520.3999999999],[1637098200000,1.0925,1.0972,1.0913,1.0947,2340053.2999999998],[1637098500000,1.0948,1.0964,1.0933,1.0951,2272554.7000000002],[1637098800000,1.0951,1.0966,1.0931,1.0941,3153594.7000000002],[1637099100000,1.094,1.096,1.0923,1.0956,2311184.7999999998],[1637099400000,1.0957,1.0966,1.0945,1.0963,1359327.6000000001],[1637099700000,1.0962,1.0969,1.0943,1.0955,1402210.6000000001],[1637100000000,1.0954,1.0999,1.0954,1.0999,2214508.5],[1637100300000,1.0999,1.1023,1.0971,1.0983,3369029.2999999998],[1637100600000,1.0984,1.1018,1.0983,1.1013,1713877.6000000001],[1637100900000,1.1014,1.1054,1.0995,1.1021,4033393.7999999998],[1637101200000,1.1021,1.1043,1.1017,1.1033,3141523.2000000002],[1637101500000,1.1033,1.1047,1.1005,1.1034,2721262.7000000002],[1637101800000,1.1034,1.104,1.1031,1.1038,907552.1],[1637102100000,1.1038,1.104,1.1005,1.1011,1635420.7],[1637102400000,1.1011,1.1013,1.1005,1.1011,828877.5],[1637102700000,1.1011,1.1014,1.0997,1.1007,1036611.5],[1637103000000,1.1008,1.1016,1.0992,1.1,755849.6],[1637103300000,1.1,1.1005,1.0991,1.0993,682736.6],[1637103600000,1.0994,1.102,1.0993,1.1009,1796997.3],[1637103900000,1.1009,1.1009,1.0976,1.0992,2076148.3],[1637104200000,1.0992,1.101,1.0958,1.0966,1887413.5],[1637104500000,1.0967,1.0976,1.0963,1.0968,1517664.7],[1637104800000,1.0967,1.0989,1.0956,1.0985,1420554.8999999999],[1637105100000,1.0984,1.0996,1.0966,1.0979,1159661.3],[1637105400000,1.098,1.0989,1.0946,1.0963,826631.7],[1637105700000,1.0962,1.0972,1.0936,1.0942,1012139.7],[1637106000000,1.0943,1.0965,1.0934,1.0952,1016698.3],[1637106300000,1.0953,1.0959,1.0914,1.0921,2394921.2999999998],[1637106600000,1.092,1.0932,1.0887,1.0924,1992608.7],[1637106900000,1.0923,1.0932,1.0875,1.0881,1619403.3],[1637107200000,1.0881,1.0923,1.0814,1.0819,7328761.2000000002],[1637107500000,1.0819,1.0949,1.0798,1.0933,7791120.0],[1637107800000,1.0933,1.0957,1.0894,1.0894,3203692.2000000002],[1637108100000,1.0893,1.0923,1.0854,1.0857,3370459.7999999998],[1637108400000,1.0856,1.0857,1.0773,1.0822,7263993.5999999996],[1637108700000,1.0821,1.0833,1.0803,1.0809,2084450.0],[1637109000000,1.0809,1.0869,1.0803,1.0845,2879224.5],[1637109300000,1.0845,1.088,1.0831,1.0854,2858814.3999999999],[1637109600000,1.0855,1.0873,1.0795,1.0819,4147523.7000000002],[1637109900000,1.0819,1.084,1.0765,1.0771,3351790.2999999998],[1637110200000,1.0771,1.0816,1.0768,1.08,2906774.5],[1637110500000,1.0799,1.083,1.0799,1.0802,2021854.0],[1637110800000,1.0803,1.0867,1.0773,1.0776,4514667.4000000004],[1637111100000,1.0776,1.0804,1.077,1.0781,2377557.0],[1637111400000,1.078,1.0852,1.0767,1.0845,2879961.7000000002],[1637111700000,1.0845,1.0857,1.0824,1.0834,1964225.8],[1637112000000,1.0833,1.0875,1.0827,1.0868,1728741.2],[1637112300000,1.0868,1.0954,1.0868,1.0949,4705829.0],[1637112600000,1.095,1.0984,1.0907,1.0968,3564621.7000000002],[1637112900000,1.0969,1.098,1.0937,1.094,3910508.6000000001],[1637113200000,1.0941,1.0956,1.0927,1.095,1694611.6000000001],[1637113500000,1.0949,1.0962,1.087,1.0871,3501061.2000000002],[1637113800000,1.0871,1.0889,1.0823,1.0835,3798244.5],[1637114100000,1.0835,1.0861,1.0775,1.0792,5097158.9000000004],[1637114400000,1.0791,1.0828,1.0757,1.0781,6593764.2999999998],[1637114700000,1.0782,1.0855,1.0777,1.085,4370758.5],[1637115000000,1.0849,1.0858,1.0815,1.0815,2163743.7000000002],[1637115300000,1.0814,1.0838,1.0785,1.0837,2472082.7000000002],[1637115600000,1.0837,1.0877,1.0837,1.086,2309508.2999999998],[1637115900000,1.086,1.0875,1.0854,1.0861,1587844.2],[1637116200000,1.086,1.088,1.0812,1.0815,2928244.2000000002],[1637116500000,1.0815,1.0869,1.0808,1.0859,2455434.7999999998],[1637116800000,1.0859,1.0864,1.0798,1.0803,1623658.6000000001],[1637117100000,1.0804,1.0827,1.079,1.0796,2038073.3999999999],[1637117400000,1.0796,1.0825,1.0733,1.0757,3769966.2999999998],[1637117700000,1.0757,1.0784,1.074,1.0763,2361069.2999999998],[1637118000000,1.0763,1.0821,1.0763,1.0778,3724170.2999999998],[1637118300000,1.0778,1.0782,1.0664,1.0695,9028147.0],[1637118600000,1.0694,1.0721,1.0629,1.0636,9485592.0999999996],[1637118900000,1.0636,1.0708,1.0584,1.0697,10075438.1999999993],[1637119200000,1.0696,1.0708,1.0669,1.0675,2935578.7000000002],[1637119500000,1.0675,1.0803,1.0667,1.0799,4704890.0999999996],[1637119800000,1.08,1.0859,1.0777,1.0822,7328433.0999999996],[1637120100000,1.0822,1.0824,1.0757,1.0786,3887194.3999999999],[1637120400000,1.0786,1.0794,1.0739,1.0767,2839375.5],[1637120700000,1.0768,1.0773,1.0723,1.0747,2334452.6000000001],[1637121000000,1.0748,1.0794,1.0725,1.0728,3459570.2999999998],[1637121300000,1.0727,1.0746,1.0674,1.0683,4035213.2999999998],[1637121600000,1.0684,1.0718,1.0638,1.0684,8054890.0],[1637121900000,1.0684,1.0726,1.0636,1.0638,3362220.7000000002],[1637122200000,1.0638,1.0664,1.0613,1.0614,2737673.2000000002],[1637122500000,1.0612,1.0698,1.0604,1.0679,6707714.5],[1637122800000,1.0678,1.0739,1.065,1.0694,5305425.5],[1637123100000,1.0694,1.0761,1.0689,1.0756,3789937.3999999999],[1637123400000,1.0756,1.079,1.0728,1.0767,4637235.0],[1637123700000,1.0767,1.0767,1.0717,1.0748,3380981.0],[1637124000000,1.0747,1.0782,1.0736,1.0738,2801660.3999999999],[1637124300000,1.0738,1.074,1.066,1.0663,3481897.7999999998],[1637124600000,1.0663,1.0689,1.0645,1.0678,2394145.7000000002],[1637124900000,1.0678,1.0768,1.0671,1.0766,3858423.2999999998],[1637125200000,1.0767,1.0817,1.073,1.0809,3924376.5],[1637125500000,1.0808,1.0833,1.0783,1.083,2728657.0],[1637125800000,1.083,1.0831,1.0795,1.0802,1779158.6000000001],[1637126100000,1.0802,1.0832,1.0786,1.0802,2990620.2999999998],[1637126400000,1.0802,1.0847,1.08,1.0846,2635455.2999999998],[1637126700000,1.0845,1.0845,1.0788,1.0797,4512611.7000000002],[1637127000000,1.0796,1.0837,1.0794,1.0822,1958104.1000000001],[1637127300000,1.0822,1.0857,1.082,1.0849,2294853.2000000002],[1637127600000,1.0849,1.0865,1.0844,1.0844,1880656.6000000001],[1637127900000,1.0844,1.0884,1.0832,1.0845,2602161.3999999999],[1637128200000,1.0846,1.0846,1.0816,1.0824,1432818.8999999999],[1637128500000,1.0824,1.0857,1.082,1.0842,1171212.2],[1637128800000,1.0843,1.086,1.0825,1.084,2035117.5],[1637129100000,1.0841,1.0848,1.0814,1.0819,1370075.7],[1637129400000,1.0819,1.082,1.0775,1.0789,2331354.0],[1637129700000,1.079,1.0801,1.0752,1.0759,1995356.3999999999],[1637130000000,1.0759,1.0798,1.0745,1.0797,2047187.6000000001],[1637130300000,1.0796,1.0796,1.0754,1.0779,2435020.2000000002],[1637130600000,1.078,1.0817,1.0779,1.0816,1979132.8999999999],[1637130900000,1.0817,1.0825,1.0802,1.0821,1801370.0],[1637131200000,1.0821,1.0821,1.0793,1.0801,1510297.2],[1637131500000,1.0802,1.081,1.0786,1.0809,1341184.1000000001],[1637131800000,1.0809,1.0811,1.0747,1.0767,3233574.1000000001],[1637132100000,1.0766,1.0819,1.0766,1.0801,3653328.0],[1637132400000,1.08,1.0841,1.08,1.0802,1885877.1000000001],[1637132700000,1.0801,1.0812,1.0762,1.0763,1922618.1000000001],[1637133000000,1.0762,1.0784,1.075,1.0754,2231747.1000000001],[1637133300000,1.0754,1.0771,1.074,1.0759,2723311.3999999999],[1637133600000,1.076,1.0782,1.0737,1.0775,2133324.0],[1637133900000,1.0775,1.0795,1.0766,1.0775,2760661.2000000002],[1637134200000,1.0775,1.0813,1.0773,1.0813,1752676.6000000001],[1637134500000,1.0812,1.0832,1.0803,1.0823,3506472.5],[1637134800000,1.0824,1.0911,1.0824,1.09,5288956.9000000004],[1637135100000,1.0899,1.0953,1.0876,1.0943,5344265.2000000002],[1637135400000,1.0942,1.0948,1.0916,1.0923,2699549.2000000002],[1637135700000,1.0922,1.0923,1.0865,1.0871,3138860.5],[1637136000000,1.0871,1.089,1.0844,1.0881,2924426.2000000002],[1637136300000,1.0881,1.0906,1.0874,1.0878,2051583.5],[1637136600000,1.0877,1.0901,1.0867,1.0899,1281969.5],[1637136900000,1.0898,1.0916,1.088,1.0903,2105461.6000000001],[1637137200000,1.0903,1.0922,1.0896,1.0897,1548503.8],[1637137500000,1.0898,1.0898,1.0858,1.0861,2406126.1000000001],[1637137800000,1.0862,1.0864,1.0808,1.0818,3634602.3999999999],[1637138100000,1.0818,1.0863,1.0818,1.0862,1386206.1000000001],[1637138400000,1.0863,1.0863,1.0836,1.0851,1375716.0],[1637138700000,1.0851,1.0884,1.0839,1.0859,2153031.5],[1637139000000,1.0859,1.0862,1.0811,1.0818,1906201.7],[1637139300000,1.0817,1.0827,1.0805,1.0806,1106265.3999999999],[1637139600000,1.0805,1.0807,1.0762,1.078,3489174.5],[1637139900000,1.078,1.0793,1.0762,1.0763,1696153.8],[1637140200000,1.0764,1.0773,1.0732,1.0742,3405487.5],[1637140500000,1.0742,1.0779,1.0725,1.0767,3377160.0],[1637140800000,1.0765,1.0797,1.0753,1.076,1986339.8],[1637141100000,1.076,1.0774,1.071,1.0718,4997953.5],[1637141400000,1.0716,1.074,1.07,1.0712,2219457.8999999999],[1637141700000,1.0712,1.0735,1.071,1.0724,1613672.6000000001],[1637142000000,1.0724,1.0736,1.0669,1.0726,4182243.6000000001],[1637142300000,1.0726,1.0753,1.0679,1.0707,5490107.2999999998],[1637142600000,1.0707,1.0744,1.0679,1.073,7715558.5999999996],[1637142900000,1.0731,1.0754,1.0684,1.0706,6229269.0999999996],[1637143200000,1.0707,1.0816,1.0706,1.0801,7898025.7000000002],[1637143500000,1.08,1.0815,1.0781,1.0803,3500217.6000000001],[1637143800000,1.0802,1.0805,1.0764,1.0782,2081784.7],[1637144100000,1.0782,1.0834,1.078,1.0831,2685664.8999999999],[1637144400000,1.083,1.0863,1.0823,1.0852,2647942.1000000001],[1637144700000,1.0852,1.0862,1.0838,1.0838,1861143.2],[1637145000000,1.0838,1.0845,1.076,1.076,4680665.9000000004],[1637145300000,1.076,1.0863,1.0703,1.0852,9234175.9000000004],[1637145600000,1.0853,1.095,1.0825,1.0899,12409553.6999999993],[1637145900000,1.0899,1.0964,1.0862,1.096,6436981.0999999996],[1637146200000,1.096,1.1068,1.096,1.106,11824100.3000000007],[1637146500000,1.106,1.1061,1.0996,1.1038,7977364.5999999996],[1637146800000,1.1039,1.106,1.0979,1.1009,7892673.9000000004],[1637147100000,1.101,1.1057,1.0986,1.1025,6771785.7999999998],[1637147400000,1.1025,1.1062,1.1018,1.1052,3735237.2000000002],[1637147700000,1.1053,1.1103,1.1028,1.1079,5009217.0],[1637148000000,1.1079,1.1084,1.1041,1.1064,3315650.8999999999],[1637148300000,1.1064,1.1069,1.1041,1.1047,3016959.2999999998],[1637148600000,1.1047,1.1058,1.1014,1.1018,3766007.3999999999],[1637148900000,1.1018,1.102,1.0981,1.1,3863771.5],[1637149200000,1.1001,1.1021,1.0994,1.1017,2386893.3999999999],[1637149500000,1.1017,1.1038,1.1007,1.1037,1990758.3999999999],[1637149800000,1.1038,1.1063,1.1032,1.1057,2846771.7000000002],[1637150100000,1.1058,1.1086,1.1048,1.1069,3226272.0],[1637150400000,1.1069,1.1081,1.1026,1.1036,3136059.2999999998],[1637150700000,1.1037,1.1062,1.1022,1.1055,2050220.6000000001],[1637151000000,1.1055,1.1074,1.1054,1.1063,1736577.5],[1637151300000,1.1063,1.1064,1.1035,1.1059,2622736.2999999998],[1637151600000,1.106,1.1068,1.1044,1.1046,2159126.8999999999],[1637151900000,1.1047,1.1059,1.1034,1.1038,1720883.5],[1637152200000,1.1037,1.1075,1.1037,1.1071,1897526.3999999999],[1637152500000,1.1071,1.1088,1.1061,1.1068,2402077.2999999998],[1637152800000,1.1069,1.107,1.1043,1.1061,2425579.7999999998],[1637153100000,1.106,1.1067,1.1039,1.1042,1281319.6000000001],[1637153400000,1.1043,1.1056,1.1042,1.1049,929455.9],[1637153700000,1.1048,1.1065,1.1048,1.1059,1360515.7],[1637154000000,1.1058,1.1133,1.1048,1.1096,6059693.5],[1637154300000,1.1096,1.1105,1.1045,1.1047,3046996.1000000001],[1637154600000,1.1048,1.1069,1.1045,1.1058,1648138.3999999999],[1637154900000,1.1059,1.1063,1.1012,1.1015,2568526.3999999999],[1637155200000,1.1015,1.1032,1.1,1.1006,2735189.7000000002],[1637155500000,1.1007,1.1033,1.1002,1.1029,1968598.8],[1637155800000,1.1028,1.1042,1.0989,1.1039,3753897.0],[1637156100000,1.1038,1.1044,1.1026,1.1038,1000207.1],[1637156400000,1.1038,1.1039,1.1001,1.1009,2153596.2999999998],[1637156700000,1.1009,1.1032,1.099,1.0995,3063064.7999999998],[1637157000000,1.0998,1.1003,1.0972,1.098,2697992.3999999999],[1637157300000,1.098,1.0992,1.096,1.096,2621387.0],[1637157600000,1.0961,1.1014,1.0961,1.1,2654646.5],[1637157900000,1.1001,1.1005,1.0981,1.0982,1341112.3999999999],[1637158200000,1.0982,1.0993,1.0965,1.0966,2182093.0],[1637158500000,1.0965,1.0992,1.0934,1.095,3821465.5],[1637158800000,1.095,1.0966,1.0936,1.095,1940575.5],[1637159100000,1.095,1.0984,1.0923,1.0977,2933823.8999999999],[1637159400000,1.0978,1.0978,1.0917,1.0923,2517615.2999999998],[1637159700000,1.0922,1.0956,1.0907,1.0914,4105409.0],[1637160000000,1.0913,1.0936,1.0906,1.0915,1618029.3999999999],[1637160300000,1.0915,1.0932,1.0886,1.0927,2769392.7999999998],[1637160600000,1.0926,1.0926,1.0847,1.0869,6282562.7999999998],[1637160900000,1.087,1.0875,1.0842,1.0846,2478907.8999999999],[1637161200000,1.0846,1.0879,1.0833,1.0874,3638166.1000000001],[1637161500000,1.0873,1.0896,1.0861,1.0868,2856420.7000000002],[1637161800000,1.0868,1.0882,1.0837,1.0838,2956741.2000000002],[1637162100000,1.0839,1.0865,1.0827,1.0845,2869705.3999999999],[1637162400000,1.0846,1.0867,1.0815,1.0827,3770542.6000000001],[1637162700000,1.0827,1.0839,1.0778,1.0804,4561579.5],[1637163000000,1.0805,1.0833,1.0756,1.0786,6138538.2999999998],[1637163300000,1.0787,1.0827,1.0783,1.0822,2007820.1000000001],[1637163600000,1.0823,1.0863,1.0816,1.085,5314252.5],[1637163900000,1.085,1.0872,1.0845,1.0871,1507044.3],[1637164200000,1.0871,1.0901,1.085,1.0855,2797640.3999999999],[1637164500000,1.0855,1.0875,1.0846,1.0861,1942630.8999999999],[1637164800000,1.0863,1.0866,1.0798,1.0808,3071536.1000000001],[1637165100000,1.0807,1.0856,1.0803,1.0854,1891225.3999999999],[1637165400000,1.0854,1.0901,1.0824,1.0901,3241084.6000000001],[1637165700000,1.0901,1.0919,1.0834,1.0834,4620817.5999999996],[1637166000000,1.0835,1.0896,1.0829,1.0875,2185543.7999999998],[1637166300000,1.0876,1.0912,1.0871,1.0906,1561475.5],[1637166600000,1.0906,1.095,1.09,1.0947,3319919.7000000002],[1637166900000,1.0947,1.0959,1.0915,1.0931,2493865.1000000001],[1637167200000,1.093,1.0943,1.0909,1.0943,2177410.2999999998],[1637167500000,1.0942,1.0963,1.0928,1.0961,1940089.1000000001],[1637167800000,1.0962,1.0963,1.0937,1.0945,1516195.5],[1637168100000,1.0946,1.0954,1.0933,1.095,1323452.1000000001],[1637168400000,1.095,1.096,1.0922,1.0927,1896410.2],[1637168700000,1.0926,1.093,1.0906,1.091,1236879.5],[1637169000000,1.0911,1.0912,1.0879,1.0888,1934815.5],[1637169300000,1.0887,1.0925,1.0879,1.0925,1552221.6000000001],[1637169600000,1.0925,1.0931,1.088,1.0903,1583392.8],[1637169900000,1.0903,1.0905,1.0882,1.0896,1510502.8999999999],[1637170200000,1.0896,1.0924,1.089,1.0923,1132094.8],[1637170500000,1.0923,1.0951,1.0919,1.0937,1582304.3],[1637170800000,1.0937,1.0941,1.0912,1.0929,1549678.0],[1637171100000,1.093,1.096,1.0925,1.0959,1505929.7],[1637171400000,1.0959,1.0984,1.0938,1.0948,2402150.2999999998],[1637171700000,1.095,1.0953,1.0928,1.0948,1127752.1000000001],[1637172000000,1.0948,1.097,1.094,1.0953,1735674.8],[1637172300000,1.0954,1.0974,1.0947,1.096,1491627.8999999999],[1637172600000,1.096,1.0969,1.0955,1.0965,664658.1],[1637172900000,1.0964,1.098,1.0959,1.0969,1453066.6000000001],[1637173200000,1.0969,1.0983,1.0967,1.0972,1340475.8],[1637173500000,1.0971,1.1006,1.0971,1.1002,1625938.8],[1637173800000,1.1001,1.1008,1.0994,1.0996,1679674.8999999999],[1637174100000,1.0997,1.0997,1.0979,1.0985,1132102.1000000001],[1637174400000,1.0985,1.0991,1.0969,1.0991,1373257.7],[1637174700000,1.099,1.1004,1.0982,1.0991,887280.9],[1637175000000,1.099,1.1023,1.099,1.1022,1543792.0],[1637175300000,1.1023,1.1032,1.0996,1.1,1377006.3],[1637175600000,1.0999,1.1031,1.0987,1.1007,2702878.7000000002],[1637175900000,1.1006,1.1012,1.0999,1.1011,799311.3],[1637176200000,1.1011,1.1015,1.0973,1.0979,2322776.1000000001],[1637176500000,1.0979,1.098,1.0959,1.0968,1159578.1000000001],[1637176800000,1.0968,1.0969,1.0947,1.095,1220533.8999999999],[1637177100000,1.0951,1.0985,1.0948,1.098,1157343.8999999999],[1637177400000,1.0979,1.098,1.0959,1.0964,929510.9],[1637177700000,1.0963,1.0986,1.0956,1.0969,1077697.0],[1637178000000,1.0968,1.0971,1.0946,1.0958,1033973.3],[1637178300000,1.0959,1.0977,1.0931,1.0934,1413592.3],[1637178600000,1.0933,1.0952,1.0926,1.0951,967092.4],[1637178900000,1.0951,1.0959,1.0942,1.0951,751371.1],[1637179200000,1.0951,1.0974,1.0933,1.0963,2208356.7999999998],[1637179500000,1.0962,1.0972,1.0945,1.0963,1179075.1000000001],[1637179800000,1.0964,1.097,1.0948,1.0949,1008579.0],[1637180100000,1.095,1.0952,1.0913,1.0917,1845474.7],[1637180400000,1.0917,1.0934,1.0906,1.0931,1408476.7],[1637180700000,1.0931,1.095,1.0931,1.0936,851841.7],[1637181000000,1.0936,1.0975,1.0927,1.0971,1295190.3999999999],[1637181300000,1.0971,1.1021,1.0971,1.1006,2684562.7000000002],[1637181600000,1.1005,1.1023,1.0997,1.102,1366607.1000000001],[1637181900000,1.102,1.1025,1.0976,1.0977,1451796.3999999999],[1637182200000,1.0976,1.1009,1.0973,1.1002,1050928.0],[1637182500000,1.1001,1.1006,1.0989,1.0999,1087095.8999999999],[1637182800000,1.1,1.1002,1.0966,1.0968,1062796.8],[1637183100000,1.0968,1.0969,1.0931,1.0936,1406189.6000000001],[1637183400000,1.0936,1.0971,1.0936,1.0959,908063.2],[1637183700000,1.096,1.0972,1.0928,1.0933,1567443.1000000001],[1637184000000,1.0933,1.0943,1.0914,1.0917,1478413.6000000001],[1637184300000,1.0916,1.0919,1.0877,1.0886,2730549.7999999998],[1637184600000,1.0885,1.0925,1.0885,1.0913,1442823.2],[1637184900000,1.0913,1.0922,1.09,1.0901,966258.9],[1637185200000,1.0902,1.0925,1.0901,1.0923,483252.6],[1637185500000,1.0923,1.0941,1.092,1.0921,760651.6],[1637185800000,1.092,1.0928,1.0904,1.091,859875.9],[1637186100000,1.091,1.0954,1.0908,1.0953,1327832.2],[1637186400000,1.0953,1.0956,1.0942,1.0943,828371.2],[1637186700000,1.0943,1.0945,1.0924,1.0932,1071065.3999999999],[1637187000000,1.0931,1.0953,1.0921,1.0949,753722.1],[1637187300000,1.0949,1.0951,1.0903,1.092,994191.6],[1637187600000,1.092,1.0926,1.0884,1.0896,1821173.5],[1637187900000,1.0896,1.0899,1.0853,1.0881,2776779.7000000002],[1637188200000,1.0882,1.089,1.0877,1.0887,673163.3],[1637188500000,1.0887,1.09,1.0887,1.09,485292.1],[1637188800000,1.0899,1.0934,1.0899,1.091,1079396.8999999999],[1637189100000,1.0909,1.0909,1.0888,1.0894,1290107.8999999999],[1637189400000,1.0894,1.0927,1.0891,1.0918,2200631.1000000001],[1637189700000,1.0916,1.093,1.0909,1.0928,564386.7],[1637190000000,1.0928,1.0948,1.0928,1.0945,747579.7],[1637190300000,1.0945,1.0948,1.0921,1.0921,969558.4],[1637190600000,1.0921,1.0921,1.0878,1.0886,1688184.8999999999],[1637190900000,1.0886,1.0909,1.0883,1.0903,844001.5],[1637191200000,1.0902,1.092,1.0902,1.0914,712580.3],[1637191500000,1.0915,1.0919,1.0903,1.0919,475440.6],[1637191800000,1.0918,1.0924,1.0902,1.0906,692121.2],[1637192100000,1.0907,1.091,1.0894,1.0895,570722.4],[1637192400000,1.0896,1.092,1.0895,1.0919,657315.2],[1637192700000,1.092,1.0975,1.0916,1.0974,1971930.8],[1637193000000,1.0975,1.0997,1.0962,1.0976,3437924.8999999999],[1637193300000,1.0977,1.0977,1.0944,1.0958,1948534.0],[1637193600000,1.0959,1.098,1.0907,1.0924,2614007.7000000002],[1637193900000,1.0922,1.1008,1.0919,1.0998,2948767.2000000002],[1637194200000,1.0997,1.1005,1.0971,1.099,2741193.2000000002],[1637194500000,1.099,1.102,1.0969,1.1005,4643687.9000000004],[1637194800000,1.1006,1.1028,1.0987,1.0998,2682431.3999999999],[1637195100000,1.0997,1.1009,1.0976,1.1001,1593940.1000000001],[1637195400000,1.1002,1.1013,1.0995,1.1011,619804.1],[1637195700000,1.1011,1.1045,1.101,1.1041,1441890.2],[1637196000000,1.1041,1.1056,1.1028,1.1041,2549423.7000000002],[1637196300000,1.1041,1.1061,1.103,1.1059,1393903.8999999999],[1637196600000,1.106,1.1072,1.1048,1.1067,1175572.8],[1637196900000,1.1068,1.1073,1.1049,1.105,1781210.8],[1637197200000,1.105,1.1061,1.1025,1.1039,2318627.7000000002],[1637197500000,1.1039,1.104,1.1027,1.1031,978156.0],[1637197800000,1.1031,1.1033,1.0992,1.1008,2558175.0],[1637198100000,1.1009,1.1019,1.0983,1.1002,3132340.3999999999],[1637198400000,1.1003,1.1013,1.0995,1.1008,788701.3],[1637198700000,1.1008,1.1043,1.1008,1.1041,1273241.0],[1637199000000,1.104,1.1377,1.104,1.1374,36479238.1000000015],[1637199300000,1.1375,1.162,1.131,1.16,37448787.799999997],[1637199600000,1.1603,1.1603,1.1405,1.1415,18739494.6999999993],[1637199900000,1.1415,1.1429,1.13,1.1348,12259697.5999999996],[1637200200000,1.1347,1.1353,1.1232,1.1252,11695103.5999999996],[1637200500000,1.1252,1.1312,1.1237,1.129,6841467.5],[1637200800000,1.129,1.1329,1.1286,1.1299,6573764.7999999998],[1637201100000,1.1298,1.1313,1.1251,1.1256,4100464.8999999999],[1637201400000,1.1256,1.1499,1.1243,1.1332,31686039.8000000007],[1637201700000,1.1331,1.1333,1.1277,1.128,4499818.2999999998],[1637202000000,1.1281,1.1294,1.1266,1.1292,2592096.3999999999],[1637202300000,1.1291,1.1311,1.1287,1.1311,1585982.8],[1637202600000,1.131,1.1393,1.1247,1.1339,11493173.5999999996],[1637202900000,1.1339,1.1364,1.1323,1.1333,3811665.3999999999],[1637203200000,1.1333,1.1342,1.1313,1.1336,2762649.7000000002],[1637203500000,1.1335,1.138,1.1324,1.1359,3023745.2000000002],[1637203800000,1.1359,1.1415,1.1357,1.1405,3071272.5],[1637204100000,1.1405,1.1405,1.1368,1.1385,2900170.0],[1637204400000,1.1385,1.1434,1.1372,1.1425,4306973.0999999996],[1637204700000,1.1424,1.1428,1.1346,1.1349,3390360.2999999998],[1637205000000,1.1348,1.1372,1.133,1.1335,3165410.2000000002],[1637205300000,1.1335,1.1375,1.1327,1.1338,1949274.1000000001],[1637205600000,1.1336,1.1356,1.1327,1.1338,1811876.2],[1637205900000,1.1338,1.1341,1.13,1.1307,4368692.7999999998],[1637206200000,1.1307,1.1325,1.1282,1.1292,3741574.2999999998],[1637206500000,1.1291,1.1322,1.1288,1.1318,2700658.2999999998],[1637206800000,1.1319,1.1321,1.1293,1.1293,1404040.3],[1637207100000,1.1293,1.1313,1.1262,1.1272,2801551.8999999999],[1637207400000,1.1271,1.1277,1.1238,1.1244,5518152.7999999998],[1637207700000,1.1245,1.1272,1.1241,1.1248,1519698.0],[1637208000000,1.1249,1.1321,1.1226,1.131,5786089.0999999996],[1637208300000,1.1309,1.133,1.1306,1.1307,1540875.6000000001],[1637208600000,1.1306,1.1322,1.1275,1.1279,2355058.6000000001],[1637208900000,1.128,1.1298,1.1261,1.1271,2771309.2999999998],[1637209200000,1.1271,1.1279,1.1254,1.1261,1854907.1000000001],[1637209500000,1.1262,1.1327,1.1169,1.1198,16188064.8000000007],[1637209800000,1.1198,1.1481,1.1165,1.1475,16896801.6000000015],[1637210100000,1.1475,1.1481,1.1322,1.1389,14173254.4000000004],[1637210400000,1.139,1.1407,1.1318,1.1364,13631417.6999999993],[1637210700000,1.1364,1.1404,1.1284,1.1294,11131764.9000000004],[1637211000000,1.1294,1.1309,1.1222,1.125,6321553.0],[1637211300000,1.125,1.1254,1.1136,1.115,11839636.9000000004],[1637211600000,1.1149,1.1176,1.1114,1.1155,6914612.5],[1637211900000,1.1153,1.1216,1.1134,1.1208,8321634.2999999998],[1637212200000,1.1208,1.1226,1.1187,1.121,5837211.4000000004],[1637212500000,1.121,1.1238,1.1181,1.1193,5989845.5999999996],[1637212800000,1.1193,1.1252,1.1169,1.1184,6576852.7999999998],[1637213100000,1.1184,1.123,1.1168,1.1219,3616199.8999999999],[1637213400000,1.1219,1.1269,1.1215,1.1219,5057255.5999999996],[1637213700000,1.1219,1.1243,1.1193,1.1198,4922280.5999999996],[1637214000000,1.1198,1.1226,1.1194,1.1194,3917341.2999999998],[1637214300000,1.1195,1.122,1.1137,1.1152,8680504.4000000004],[1637214600000,1.1153,1.1162,1.1118,1.1156,5167395.4000000004],[1637214900000,1.1156,1.1183,1.1117,1.1142,6087973.5],[1637215200000,1.1142,1.1153,1.1111,1.1131,4553846.2999999998],[1637215500000,1.1132,1.1134,1.1084,1.1087,4937834.5],[1637215800000,1.1087,1.1104,1.1064,1.1082,4350161.0],[1637216100000,1.1081,1.114,1.106,1.1139,4203990.5999999996],[1637216400000,1.114,1.1151,1.1102,1.1145,3909556.7000000002],[1637216700000,1.1145,1.1164,1.1123,1.1134,2783403.1000000001],[1637217000000,1.1133,1.1143,1.1104,1.1111,1793417.8],[1637217300000,1.1111,1.1175,1.111,1.1168,2683384.2000000002],[1637217600000,1.1168,1.1168,1.1129,1.1144,1596005.3],[1637217900000,1.1143,1.1143,1.1102,1.1124,1885052.7],[1637218200000,1.1121,1.1136,1.1086,1.1088,1878341.2],[1637218500000,1.1089,1.1145,1.1076,1.1142,2420014.8999999999],[1637218800000,1.1142,1.1147,1.1087,1.1096,3307754.7999999998],[1637219100000,1.1095,1.112,1.1086,1.1104,2543497.0],[1637219400000,1.1104,1.1104,1.1056,1.1059,2295635.2000000002],[1637219700000,1.1058,1.1079,1.1034,1.1073,2723064.3999999999],[1637220000000,1.1074,1.1078,1.103,1.1044,2910400.1000000001],[1637220300000,1.1043,1.1045,1.1022,1.103,2304491.7999999998],[1637220600000,1.1031,1.1052,1.099,1.1009,6195101.9000000004],[1637220900000,1.1009,1.1024,1.0966,1.1015,3942212.8999999999],[1637221200000,1.1015,1.1064,1.1004,1.1058,3304070.8999999999],[1637221500000,1.1057,1.1076,1.1043,1.1064,2473126.7000000002],[1637221800000,1.1065,1.1079,1.1044,1.105,2062578.0],[1637222100000,1.105,1.1078,1.1048,1.1074,1259992.1000000001],[1637222400000,1.1075,1.1104,1.1052,1.1056,3241738.8999999999],[1637222700000,1.1056,1.1082,1.1038,1.1055,2764367.7999999998],[1637223000000,1.1055,1.1065,1.103,1.1033,2205122.6000000001],[1637223300000,1.1034,1.1082,1.1022,1.1078,1854984.3999999999],[1637223600000,1.1077,1.1093,1.1062,1.1073,1405444.0],[1637223900000,1.1074,1.1097,1.107,1.109,1632723.5],[1637224200000,1.109,1.1103,1.1074,1.1078,2075103.8],[1637224500000,1.1079,1.1079,1.104,1.1045,1888507.0],[1637224800000,1.1045,1.1053,1.1033,1.1037,1495111.3999999999],[1637225100000,1.1038,1.1082,1.1028,1.1043,4119453.2999999998],[1637225400000,1.1043,1.107,1.1037,1.1042,1954533.8],[1637225700000,1.1043,1.1079,1.1038,1.1072,1478600.3999999999],[1637226000000,1.107,1.1077,1.1032,1.1035,3126430.7999999998],[1637226300000,1.1035,1.105,1.0976,1.0985,6405633.7000000002],[1637226600000,1.0986,1.0999,1.083,1.0906,12260258.1999999993],[1637226900000,1.0906,1.0923,1.0866,1.089,5197434.7999999998],[1637227200000,1.0891,1.0917,1.0814,1.0839,7086193.5],[1637227500000,1.0839,1.0857,1.0824,1.0853,3046616.2000000002],[1637227800000,1.0853,1.0882,1.079,1.0845,5761242.0999999996],[1637228100000,1.0845,1.0847,1.078,1.084,5052047.0999999996],[1637228400000,1.084,1.0874,1.0831,1.0846,3199587.5],[1637228700000,1.0845,1.0872,1.0815,1.0828,2354368.8999999999],[1637229000000,1.0828,1.0869,1.0811,1.0819,2610370.3999999999],[1637229300000,1.0819,1.0898,1.0773,1.0869,8080943.0],[1637229600000,1.0869,1.0929,1.086,1.0927,4635765.2999999998],[1637229900000,1.0927,1.0927,1.0864,1.0876,2974256.7000000002],[1637230200000,1.0877,1.0878,1.0831,1.0848,2237304.3999999999],[1637230500000,1.0847,1.0858,1.0811,1.0849,2963489.2999999998],[1637230800000,1.085,1.0913,1.081,1.0892,3821236.0],[1637231100000,1.0893,1.0901,1.0872,1.0894,2304758.1000000001],[1637231400000,1.0894,1.0948,1.0886,1.0946,2668053.0],[1637231700000,1.0945,1.095,1.0913,1.0918,2534872.2000000002],[1637232000000,1.0919,1.0944,1.091,1.0942,1711755.1000000001],[1637232300000,1.0941,1.0955,1.0917,1.0944,2627556.3999999999],[1637232600000,1.0944,1.0973,1.0929,1.0973,1769328.3],[1637232900000,1.0973,1.099,1.0954,1.0979,2591498.7000000002],[1637233200000,1.098,1.1027,1.0947,1.1025,5480946.7999999998],[1637233500000,1.1025,1.1025,1.0985,1.099,3196790.7999999998],[1637233800000,1.099,1.0999,1.0973,1.0993,1710833.5],[1637234100000,1.0994,1.0996,1.0954,1.096,2155253.1000000001],[1637234400000,1.096,1.0962,1.0944,1.0946,1713920.6000000001],[1637234700000,1.0947,1.0984,1.0942,1.0973,1729530.2],[1637235000000,1.0973,1.0973,1.0919,1.0926,3354500.5],[1637235300000,1.0925,1.0933,1.0907,1.0914,2148017.6000000001],[1637235600000,1.0914,1.0946,1.0904,1.0909,3060508.7999999998],[1637235900000,1.0909,1.0914,1.0871,1.0888,3127402.1000000001],[1637236200000,1.0886,1.0926,1.0883,1.089,2654773.6000000001],[1637236500000,1.0889,1.089,1.0837,1.085,3756011.2000000002],[1637236800000,1.085,1.0884,1.0818,1.0876,3740646.8999999999],[1637237100000,1.0875,1.0881,1.0813,1.0873,5464295.0],[1637237400000,1.0874,1.0915,1.086,1.0914,2984681.8999999999],[1637237700000,1.0915,1.0955,1.0862,1.0941,4439409.5],[1637238000000,1.0941,1.0954,1.0892,1.0892,4380673.9000000004],[1637238300000,1.0892,1.0897,1.0864,1.0888,2976525.6000000001],[1637238600000,1.0888,1.0913,1.0867,1.0867,1701390.8999999999],[1637238900000,1.0867,1.0877,1.0842,1.0851,2283647.6000000001],[1637239200000,1.0851,1.0866,1.0838,1.0862,1721140.5],[1637239500000,1.0862,1.0898,1.0859,1.0896,1310288.6000000001],[1637239800000,1.0896,1.09,1.0804,1.0807,4536972.0999999996],[1637240100000,1.0807,1.0853,1.0801,1.0838,4762274.5],[1637240400000,1.0836,1.0848,1.0792,1.0804,5064635.0],[1637240700000,1.0804,1.0831,1.08,1.0813,1537403.2],[1637241000000,1.0812,1.0815,1.0747,1.077,5156479.2999999998],[1637241300000,1.0771,1.0797,1.07,1.0747,7183726.9000000004],[1637241600000,1.0747,1.0757,1.0707,1.0731,4569661.2000000002],[1637241900000,1.073,1.0735,1.066,1.0711,6827627.2000000002],[1637242200000,1.0712,1.0744,1.0668,1.0736,4523276.2000000002],[1637242500000,1.0735,1.088,1.0732,1.087,10117740.1999999993],[1637242800000,1.087,1.0877,1.0814,1.083,6775660.9000000004],[1637243100000,1.0829,1.0851,1.081,1.0821,2642695.7000000002],[1637243400000,1.0821,1.0824,1.075,1.0761,4880387.2000000002],[1637243700000,1.0761,1.0778,1.0743,1.0772,1534373.2],[1637244000000,1.0771,1.0772,1.0716,1.0745,4327706.2000000002],[1637244300000,1.0744,1.0813,1.074,1.0803,3020688.8999999999],[1637244600000,1.0803,1.0806,1.0731,1.0736,3849244.6000000001],[1637244900000,1.0735,1.0753,1.0702,1.0725,2651872.2000000002],[1637245200000,1.0725,1.0759,1.0704,1.0754,2410886.6000000001],[1637245500000,1.0754,1.0761,1.0719,1.0719,1779660.3999999999],[1637245800000,1.0719,1.078,1.0715,1.0778,2666691.0],[1637246100000,1.0778,1.0779,1.0727,1.0743,2677487.3999999999],[1637246400000,1.0744,1.0755,1.0691,1.0694,2987468.6000000001],[1637246700000,1.0694,1.0744,1.0678,1.0681,4545253.0999999996],[1637247000000,1.0681,1.0718,1.0673,1.0689,3032031.3999999999],[1637247300000,1.0688,1.0771,1.0668,1.0713,4745771.5],[1637247600000,1.0713,1.077,1.0704,1.0714,5149637.0],[1637247900000,1.0714,1.0737,1.069,1.0698,2278352.5],[1637248200000,1.0698,1.0698,1.059,1.0638,12208164.5999999996],[1637248500000,1.0637,1.0642,1.0562,1.0603,10020211.0999999996],[1637248800000,1.0603,1.0631,1.0507,1.052,17936300.8999999985],[1637249100000,1.0516,1.0667,1.045,1.0659,26789320.8000000007],[1637249400000,1.0659,1.0753,1.0643,1.0722,15230670.6999999993],[1637249700000,1.0721,1.0784,1.0719,1.0756,7623911.2999999998],[1637250000000,1.0756,1.0769,1.0651,1.0654,8434091.4000000004],[1637250300000,1.0652,1.0701,1.0603,1.0618,9904451.5],[1637250600000,1.0618,1.0639,1.0547,1.056,9672806.5],[1637250900000,1.0558,1.0589,1.051,1.0563,7416348.5999999996],[1637251200000,1.0564,1.0607,1.0503,1.0544,9694105.0],[1637251500000,1.0545,1.0562,1.0499,1.0551,8325909.5],[1637251800000,1.055,1.0635,1.054,1.0619,7034183.5999999996],[1637252100000,1.0618,1.0635,1.0535,1.0551,4967031.9000000004],[1637252400000,1.0551,1.0551,1.0428,1.0489,12212691.6999999993],[1637252700000,1.0493,1.0531,1.0405,1.0499,12684642.0999999996],[1637253000000,1.05,1.0524,1.043,1.0442,9199165.8000000007],[1637253300000,1.0442,1.0521,1.0419,1.0483,7289009.9000000004],[1637253600000,1.0483,1.056,1.0461,1.0495,6475553.9000000004],[1637253900000,1.0495,1.0502,1.0434,1.0464,6323164.9000000004],[1637254200000,1.0465,1.0522,1.044,1.0513,4960452.7999999998],[1637254500000,1.0513,1.0515,1.0394,1.0407,6911517.2000000002],[1637254800000,1.0407,1.0444,1.0296,1.0399,23939978.1000000015],[1637255100000,1.0398,1.0405,1.0256,1.026,11352941.5999999996],[1637255400000,1.026,1.03,1.0145,1.0222,30418058.1999999993],[1637255700000,1.022,1.0407,1.0219,1.0405,14897561.9000000004],[1637256000000,1.0405,1.048,1.0354,1.0464,9762881.9000000004],[1637256300000,1.0467,1.0479,1.0434,1.0462,7487226.4000000004],[1637256600000,1.0463,1.0514,1.0445,1.0475,6550172.2000000002],[1637256900000,1.0474,1.0503,1.0425,1.05,6146483.4000000004],[1637257200000,1.0501,1.051,1.046,1.0487,3916486.5],[1637257500000,1.0487,1.0534,1.0428,1.0447,7068007.0],[1637257800000,1.0447,1.0452,1.0408,1.0437,3858984.3999999999],[1637258100000,1.0436,1.0466,1.0388,1.0421,8796545.6999999993],[1637258400000,1.042,1.0457,1.0415,1.0445,3286390.3999999999],[1637258700000,1.0445,1.0484,1.0414,1.0474,3499227.2000000002],[1637259000000,1.0475,1.0491,1.0441,1.0449,2073718.3999999999],[1637259300000,1.0449,1.0484,1.0443,1.0448,1992172.8999999999],[1637259600000,1.0447,1.0532,1.043,1.0517,7335197.7000000002],[1637259900000,1.0513,1.0527,1.0477,1.0481,2694719.1000000001],[1637260200000,1.0481,1.0546,1.0464,1.0535,2788362.8999999999],[1637260500000,1.0533,1.0546,1.048,1.0496,3148787.3999999999],[1637260800000,1.0495,1.0495,1.0471,1.0483,1636203.7],[1637261100000,1.0484,1.052,1.0478,1.0503,2142292.8999999999],[1637261400000,1.0503,1.0521,1.0474,1.052,1521865.5],[1637261700000,1.0521,1.0533,1.0502,1.052,1615455.0],[1637262000000,1.052,1.0534,1.0504,1.0511,2500315.7000000002],[1637262300000,1.0512,1.0533,1.0492,1.0525,1731929.8999999999],[1637262600000,1.0525,1.0526,1.0509,1.0525,1288238.6000000001],[1637262900000,1.0526,1.0534,1.0511,1.0521,1981181.3],[1637263200000,1.0522,1.0549,1.0517,1.0544,2082135.0],[1637263500000,1.0544,1.0589,1.0531,1.0538,4354132.7000000002],[1637263800000,1.0538,1.0592,1.0534,1.0591,3016289.7000000002],[1637264100000,1.0592,1.0597,1.0567,1.0582,2208937.5],[1637264400000,1.0582,1.0584,1.055,1.0559,1592049.6000000001],[1637264700000,1.0559,1.0559,1.05,1.0546,4724945.0999999996],[1637265000000,1.0545,1.0564,1.0541,1.0549,1248205.2],[1637265300000,1.055,1.0561,1.0529,1.0555,1044990.1],[1637265600000,1.0556,1.0562,1.051,1.0511,1374822.2],[1637265900000,1.0512,1.0529,1.0486,1.0503,2055764.1000000001],[1637266200000,1.0502,1.0536,1.0486,1.0523,2311715.2000000002],[1637266500000,1.0524,1.053,1.048,1.0529,1536118.8],[1637266800000,1.0529,1.0548,1.0523,1.0547,1741091.2],[1637267100000,1.0547,1.0549,1.0503,1.051,1699248.0],[1637267400000,1.0511,1.0522,1.0497,1.0499,1616389.8999999999],[1637267700000,1.0499,1.0546,1.0497,1.0542,1268396.3],[1637268000000,1.0543,1.0556,1.0519,1.0544,1719555.8],[1637268300000,1.0543,1.0595,1.054,1.0589,2433730.2000000002],[1637268600000,1.0589,1.0596,1.0554,1.0579,2604349.6000000001],[1637268900000,1.0579,1.0586,1.0473,1.0486,5917066.2999999998],[1637269200000,1.0486,1.0555,1.0482,1.0549,3221514.0],[1637269500000,1.0549,1.0558,1.0517,1.0517,1468725.8999999999],[1637269800000,1.0517,1.0565,1.0517,1.0542,1610096.8999999999],[1637270100000,1.0543,1.0548,1.0518,1.0519,848440.7],[1637270400000,1.052,1.0533,1.0486,1.049,1920701.8],[1637270700000,1.049,1.0504,1.0486,1.0488,784432.5],[1637271000000,1.0488,1.0495,1.042,1.048,7191645.5],[1637271300000,1.048,1.0493,1.0455,1.0458,1702880.2],[1637271600000,1.0457,1.0474,1.0451,1.0454,1642083.3],[1637271900000,1.0454,1.0473,1.0434,1.0465,1665604.3],[1637272200000,1.0464,1.0477,1.0415,1.0445,2543931.0],[1637272500000,1.0445,1.0463,1.0425,1.0455,3287773.8999999999],[1637272800000,1.0454,1.0468,1.0436,1.0437,2387173.8999999999],[1637273100000,1.0438,1.0454,1.0404,1.0422,2824927.5],[1637273400000,1.0421,1.0471,1.0337,1.0468,8781184.3000000007],[1637273700000,1.0467,1.0486,1.0425,1.0457,3452063.3999999999],[1637274000000,1.0458,1.0466,1.0421,1.046,1607743.3999999999],[1637274300000,1.046,1.0481,1.0459,1.0474,1390902.8999999999],[1637274600000,1.0473,1.0526,1.0459,1.0525,1985031.0],[1637274900000,1.0526,1.054,1.0464,1.0466,2085753.3],[1637275200000,1.0466,1.0466,1.0414,1.0439,1734988.8999999999],[1637275500000,1.044,1.0473,1.039,1.0423,2637924.2000000002],[1637275800000,1.0423,1.045,1.0403,1.041,1501560.1000000001],[1637276100000,1.041,1.0458,1.0364,1.0393,3287501.5],[1637276400000,1.0394,1.0408,1.0354,1.037,3141329.3999999999],[1637276700000,1.037,1.0437,1.0365,1.041,2409815.2999999998],[1637277000000,1.041,1.0412,1.0346,1.0362,2711584.7000000002],[1637277300000,1.0362,1.0383,1.0327,1.0334,2474262.7999999998],[1637277600000,1.0334,1.043,1.0329,1.0419,3130971.7999999998],[1637277900000,1.0419,1.0439,1.04,1.0434,3426939.5],[1637278200000,1.0434,1.0464,1.0363,1.0371,3060759.8999999999],[1637278500000,1.0371,1.0412,1.0333,1.0411,2772434.5],[1637278800000,1.0411,1.0411,1.033,1.0358,2457532.7000000002],[1637279100000,1.0359,1.0388,1.0286,1.038,5787787.9000000004],[1637279400000,1.0379,1.04,1.033,1.0345,2665939.3999999999],[1637279700000,1.0345,1.0438,1.034,1.041,4038551.6000000001],[1637280000000,1.0411,1.0464,1.0314,1.0345,9002812.8000000007],[1637280300000,1.0346,1.0416,1.0295,1.0415,5701463.4000000004],[1637280600000,1.0415,1.0479,1.038,1.0438,8495110.0999999996],[1637280900000,1.0438,1.0482,1.0422,1.0446,3843031.8999999999],[1637281200000,1.0446,1.0446,1.0379,1.0394,5906549.0999999996],[1637281500000,1.0395,1.0433,1.0382,1.0429,1947493.5],[1637281800000,1.0429,1.0489,1.0396,1.0475,4989048.5],[1637282100000,1.0475,1.053,1.0475,1.0512,4821880.0999999996],[1637282400000,1.0511,1.0572,1.0483,1.0564,4416235.7000000002],[1637282700000,1.0565,1.0568,1.0505,1.0509,3597824.1000000001],[1637283000000,1.051,1.0546,1.0496,1.0524,2647578.2999999998],[1637283300000,1.0525,1.0563,1.0514,1.0551,3312801.0],[1637283600000,1.055,1.0557,1.0499,1.0517,3101542.5],[1637283900000,1.0517,1.0526,1.0499,1.0524,2397938.1000000001],[1637284200000,1.0524,1.0526,1.0495,1.0512,2091210.3999999999],[1637284500000,1.0512,1.052,1.0475,1.0486,2411746.1000000001],[1637284800000,1.0484,1.0496,1.046,1.0482,2792944.1000000001],[1637285100000,1.0482,1.0483,1.0442,1.0443,1568003.0],[1637285400000,1.0443,1.0463,1.0439,1.0449,1622501.3999999999],[1637285700000,1.0449,1.0464,1.0442,1.0461,1144991.3999999999],[1637286000000,1.0461,1.0477,1.04,1.0405,3626138.2000000002],[1637286300000,1.0404,1.0423,1.0385,1.0405,3132361.7000000002],[1637286600000,1.0402,1.0416,1.0365,1.037,2028380.8],[1637286900000,1.037,1.0401,1.0356,1.0359,2847903.7999999998],[1637287200000,1.0358,1.0392,1.0345,1.0386,3284689.1000000001],[1637287500000,1.0384,1.0389,1.0357,1.037,1779480.5],[1637287800000,1.0371,1.0432,1.036,1.0428,2659037.3999999999],[1637288100000,1.0428,1.0442,1.0407,1.043,1587892.0],[1637288400000,1.043,1.0459,1.0429,1.045,1169724.5],[1637288700000,1.045,1.0452,1.0381,1.0387,3473844.7000000002],[1637289000000,1.0387,1.0407,1.0378,1.0403,1250291.6000000001],[1637289300000,1.0402,1.0416,1.038,1.038,1183740.0],[1637289600000,1.0381,1.039,1.03,1.0306,4775244.7999999998],[1637289900000,1.0306,1.0332,1.0256,1.028,5871642.9000000004],[1637290200000,1.028,1.0282,1.0211,1.0219,8272004.7999999998],[1637290500000,1.0221,1.0272,1.0201,1.0248,8520749.6999999993],[1637290800000,1.0248,1.0274,1.0232,1.0265,5266811.5999999996],[1637291100000,1.0265,1.0275,1.0221,1.0268,3088035.5],[1637291400000,1.0269,1.0294,1.0259,1.028,2541298.6000000001],[1637291700000,1.028,1.0285,1.0236,1.0241,2174009.6000000001],[1637292000000,1.0242,1.026,1.0229,1.0229,1710564.1000000001],[1637292300000,1.0229,1.0253,1.0216,1.0238,2250729.0],[1637292600000,1.0238,1.0252,1.0202,1.0251,5006980.7999999998],[1637292900000,1.025,1.0271,1.0234,1.0252,2973786.5],[1637293200000,1.0252,1.0252,1.0179,1.0191,4405564.2999999998],[1637293500000,1.019,1.0228,1.019,1.0224,2165756.7999999998],[1637293800000,1.0225,1.0272,1.0219,1.027,3005237.2000000002],[1637294100000,1.0269,1.029,1.0251,1.0289,2614775.7000000002],[1637294400000,1.0289,1.0336,1.0261,1.033,5683874.0],[1637294700000,1.033,1.0339,1.0302,1.0316,2516339.0],[1637295000000,1.0316,1.0338,1.0302,1.0335,2668964.7000000002],[1637295300000,1.0333,1.0388,1.0327,1.037,2691412.1000000001],[1637295600000,1.0369,1.0386,1.0359,1.0373,1374237.5],[1637295900000,1.0372,1.0387,1.0365,1.038,1478740.8],[1637296200000,1.038,1.0404,1.0366,1.0394,1943930.3999999999],[1637296500000,1.0395,1.0399,1.0378,1.0398,805676.1],[1637296800000,1.0399,1.0413,1.0382,1.0382,3092379.3999999999],[1637297100000,1.0383,1.0412,1.0377,1.0401,2541816.3999999999],[1637297400000,1.04,1.0413,1.0392,1.0408,1096755.3999999999],[1637297700000,1.0407,1.0451,1.0407,1.0432,4498930.5999999996],[1637298000000,1.0433,1.0443,1.042,1.0421,1396137.1000000001],[1637298300000,1.042,1.0449,1.042,1.0435,1937069.1000000001],[1637298600000,1.0436,1.0446,1.0428,1.0437,1266308.0],[1637298900000,1.0437,1.0444,1.0399,1.0401,2101957.1000000001],[1637299200000,1.0401,1.0413,1.0376,1.0382,2169149.0],[1637299500000,1.0382,1.0388,1.0371,1.0376,1569698.2],[1637299800000,1.0377,1.0439,1.0375,1.0435,2813737.7000000002],[1637300100000,1.0435,1.0466,1.0409,1.042,4241173.2000000002],[1637300400000,1.0421,1.0445,1.0421,1.0437,2477144.1000000001],[1637300700000,1.0437,1.0458,1.0436,1.0444,1621875.8999999999],[1637301000000,1.0444,1.0459,1.0441,1.0458,1097824.8999999999],[1637301300000,1.0459,1.0469,1.0446,1.045,1755199.3],[1637301600000,1.0451,1.0461,1.043,1.0444,1810849.2],[1637301900000,1.0444,1.0498,1.0439,1.0483,2074098.8],[1637302200000,1.0484,1.0495,1.0461,1.0464,1457180.2],[1637302500000,1.0464,1.0479,1.046,1.0466,1174520.3999999999],[1637302800000,1.0467,1.0468,1.0428,1.043,1564176.3],[1637303100000,1.0431,1.0467,1.0429,1.0454,2964933.3999999999],[1637303400000,1.0454,1.0459,1.0438,1.0454,766589.1],[1637303700000,1.0454,1.0484,1.0453,1.0475,1310481.5],[1637304000000,1.0476,1.0476,1.0423,1.0427,2213511.5],[1637304300000,1.0427,1.0456,1.0425,1.0438,1353233.3],[1637304600000,1.0437,1.0443,1.0411,1.043,1957829.1000000001],[1637304900000,1.0431,1.0459,1.0426,1.0448,1671180.0],[1637305200000,1.0448,1.0488,1.0447,1.0467,2016082.3999999999],[1637305500000,1.0467,1.0468,1.0416,1.0416,1787466.0],[1637305800000,1.0416,1.0433,1.0404,1.0405,1569913.6000000001],[1637306100000,1.0405,1.0411,1.0382,1.0393,1937543.5],[1637306400000,1.0392,1.042,1.0392,1.0413,1031926.7],[1637306700000,1.0413,1.0414,1.0381,1.0404,1714814.1000000001],[1637307000000,1.0404,1.0426,1.0392,1.0393,1783571.5],[1637307300000,1.0393,1.0404,1.0384,1.0403,1364430.2],[1637307600000,1.0402,1.0431,1.0389,1.0426,1227451.1000000001],[1637307900000,1.0426,1.0443,1.0397,1.04,1626605.7],[1637308200000,1.0401,1.0421,1.0387,1.0416,1494791.8999999999],[1637308500000,1.0417,1.0429,1.039,1.0421,1559077.3999999999],[1637308800000,1.042,1.0474,1.0418,1.0424,4172338.0],[1637309100000,1.0423,1.0473,1.0423,1.0467,2169063.3999999999],[1637309400000,1.0468,1.0486,1.0466,1.0484,1915057.7],[1637309700000,1.0483,1.0568,1.0483,1.0563,6329233.0999999996],[1637310000000,1.0563,1.0661,1.0544,1.0631,11705595.4000000004],[1637310300000,1.0632,1.0637,1.0589,1.0626,5223845.4000000004],[1637310600000,1.0625,1.063,1.0582,1.0592,3978271.3999999999],[1637310900000,1.0592,1.0621,1.0582,1.0612,2589196.5],[1637311200000,1.0612,1.0619,1.0589,1.0597,1740018.7],[1637311500000,1.0597,1.0631,1.0572,1.0577,2488892.8999999999],[1637311800000,1.0578,1.059,1.0564,1.0569,1876773.3],[1637312100000,1.0568,1.058,1.0555,1.057,2283765.7999999998],[1637312400000,1.0569,1.0589,1.0562,1.0572,2233281.2999999998],[1637312700000,1.0573,1.0608,1.057,1.0596,2130616.0],[1637313000000,1.0597,1.062,1.0596,1.0615,1626153.3],[1637313300000,1.0615,1.0649,1.0607,1.0631,3424118.8999999999],[1637313600000,1.0631,1.0646,1.0612,1.0623,2235360.2000000002],[1637313900000,1.0623,1.0624,1.0594,1.0609,2156914.3999999999],[1637314200000,1.061,1.0621,1.0596,1.0599,1578938.8],[1637314500000,1.0598,1.0619,1.0597,1.0611,1440036.3],[1637314800000,1.061,1.0627,1.061,1.0625,1264797.3999999999],[1637315100000,1.0626,1.0627,1.0598,1.06,1445057.8999999999],[1637315400000,1.0601,1.0621,1.0594,1.0597,1705156.6000000001],[1637315700000,1.0597,1.0603,1.0569,1.0583,2514239.6000000001],[1637316000000,1.0581,1.0592,1.0554,1.0554,1798072.6000000001],[1637316300000,1.0554,1.0566,1.0547,1.055,1512323.8],[1637316600000,1.055,1.0552,1.0532,1.0534,1732325.8999999999],[1637316900000,1.0534,1.0554,1.0518,1.0535,2882577.7000000002],[1637317200000,1.0536,1.0567,1.0536,1.0566,1118497.6000000001],[1637317500000,1.0565,1.057,1.0558,1.0566,1016241.9],[1637317800000,1.0566,1.0569,1.0547,1.0548,1407670.7],[1637318100000,1.0547,1.0553,1.0524,1.0524,2160236.7000000002],[1637318400000,1.0524,1.0543,1.0494,1.0505,8216318.2000000002],[1637318700000,1.0506,1.0538,1.0505,1.0535,2185536.2000000002],[1637319000000,1.0535,1.0538,1.0519,1.053,973366.5],[1637319300000,1.0529,1.055,1.0515,1.0549,1076603.3999999999],[1637319600000,1.0549,1.058,1.0516,1.0526,3025784.3999999999],[1637319900000,1.0528,1.0543,1.0526,1.0532,858447.4],[1637320200000,1.0532,1.0535,1.0464,1.0471,2844673.6000000001],[1637320500000,1.0471,1.0499,1.047,1.0486,1777340.6000000001],[1637320800000,1.0486,1.0499,1.0456,1.0494,2930679.3999999999],[1637321100000,1.0495,1.0512,1.0485,1.0489,1468135.5],[1637321400000,1.0489,1.0514,1.0476,1.0482,2117825.8999999999],[1637321700000,1.0482,1.0506,1.0469,1.05,1484764.5],[1637322000000,1.0499,1.0549,1.0492,1.0526,2848893.7999999998],[1637322300000,1.0526,1.0526,1.0492,1.0502,1876539.3999999999],[1637322600000,1.0503,1.0516,1.0502,1.0512,1289668.8999999999],[1637322900000,1.0512,1.0544,1.0512,1.0538,1193687.5],[1637323200000,1.0538,1.0557,1.0519,1.054,2278614.5],[1637323500000,1.054,1.055,1.0517,1.0538,1639711.8],[1637323800000,1.0539,1.0588,1.0538,1.0583,2866716.7999999998],[1637324100000,1.0582,1.0603,1.0567,1.0599,2282008.2000000002],[1637324400000,1.0598,1.0599,1.0568,1.0581,1824810.7],[1637324700000,1.058,1.061,1.0573,1.0607,2480658.6000000001],[1637325000000,1.0607,1.0608,1.0574,1.0588,1958265.1000000001],[1637325300000,1.0589,1.0601,1.0579,1.0582,1227657.2],[1637325600000,1.0583,1.0596,1.0564,1.0574,1629522.8999999999],[1637325900000,1.0574,1.0584,1.055,1.0572,1654087.0],[1637326200000,1.0572,1.0573,1.0537,1.0544,2593881.6000000001],[1637326500000,1.0545,1.0556,1.0523,1.0528,1929621.1000000001],[1637326800000,1.0528,1.055,1.0495,1.0516,2965527.8999999999],[1637327100000,1.0517,1.0558,1.0514,1.0542,2262686.0],[1637327400000,1.0542,1.0574,1.0532,1.0573,1695916.1000000001],[1637327700000,1.0572,1.0598,1.0552,1.0586,2219687.1000000001],[1637328000000,1.0586,1.0605,1.0571,1.0601,2067080.6000000001],[1637328300000,1.0602,1.0619,1.0585,1.0598,3917029.8999999999],[1637328600000,1.0599,1.0619,1.0591,1.0607,2356014.2999999998],[1637328900000,1.0607,1.0641,1.0602,1.0641,2933633.6000000001],[1637329200000,1.0641,1.0649,1.0608,1.0622,3647307.0],[1637329500000,1.0622,1.0652,1.0622,1.0645,2560422.5],[1637329800000,1.0646,1.065,1.0625,1.0642,1783036.3],[1637330100000,1.0643,1.0654,1.063,1.0651,1559591.0],[1637330400000,1.0652,1.0679,1.0652,1.0669,3404484.0],[1637330700000,1.0669,1.0693,1.0668,1.0679,2853599.2000000002],[1637331000000,1.0679,1.0702,1.0671,1.0697,2969501.7999999998],[1637331300000,1.0696,1.0704,1.0665,1.0685,2572050.3999999999],[1637331600000,1.0685,1.086,1.0685,1.075,14928506.0],[1637331900000,1.0751,1.0775,1.0704,1.0724,5195830.5],[1637332200000,1.0724,1.0729,1.0672,1.0674,3428569.8999999999],[1637332500000,1.0675,1.0702,1.067,1.068,1760836.1000000001],[1637332800000,1.068,1.0744,1.068,1.0738,2232547.3999999999],[1637333100000,1.0739,1.077,1.0726,1.077,2434194.2000000002],[1637333400000,1.077,1.1028,1.0742,1.0919,24851780.8999999985],[1637333700000,1.0919,1.1034,1.0905,1.095,17220289.8999999985],[1637334000000,1.0949,1.0993,1.0886,1.0937,10645506.5],[1637334300000,1.0936,1.0936,1.0857,1.0884,4950515.2999999998],[1637334600000,1.0884,1.0894,1.0832,1.0849,4678329.9000000004],[1637334900000,1.0847,1.0871,1.0815,1.0868,4151758.8999999999],[1637335200000,1.0868,1.0986,1.0856,1.09,9628256.9000000004],[1637335500000,1.0899,1.0907,1.0858,1.0864,2832302.7999999998],[1637335800000,1.0864,1.0913,1.0854,1.0864,4114308.1000000001],[1637336100000,1.0863,1.0996,1.0863,1.0913,12850899.4000000004],[1637336400000,1.0914,1.0923,1.0889,1.0903,2702810.0],[1637336700000,1.0902,1.0905,1.0867,1.0896,2937454.6000000001],[1637337000000,1.0897,1.0933,1.089,1.0914,3057155.3999999999],[1637337300000,1.0913,1.0915,1.0882,1.0891,1900239.8999999999],[1637337600000,1.0891,1.0934,1.0885,1.0918,3386471.8999999999],[1637337900000,1.0918,1.0949,1.0892,1.0943,2656613.2999999998],[1637338200000,1.0943,1.0944,1.0903,1.0907,2280753.5],[1637338500000,1.0906,1.0928,1.0886,1.0896,2880637.2999999998],[1637338800000,1.0896,1.0902,1.0748,1.0809,15229325.0999999996],[1637339100000,1.0809,1.0825,1.0801,1.0815,3759337.2999999998],[1637339400000,1.0815,1.0859,1.0815,1.0841,3276425.1000000001],[1637339700000,1.0842,1.0879,1.0831,1.0866,2705449.7999999998],[1637340000000,1.0867,1.0879,1.0851,1.0867,1940085.5],[1637340300000,1.0867,1.0877,1.0843,1.0857,1990591.6000000001],[1637340600000,1.0857,1.0901,1.0856,1.0899,1749199.3999999999],[1637340900000,1.0899,1.0914,1.0896,1.0909,2214512.1000000001],[1637341200000,1.0909,1.099,1.0904,1.0954,4852518.7000000002],[1637341500000,1.0954,1.0972,1.0931,1.0962,2662937.7000000002],[1637341800000,1.0961,1.0985,1.0956,1.0963,2422019.5],[1637342100000,1.0963,1.098,1.0953,1.0954,1835799.8],[1637342400000,1.0953,1.0963,1.0917,1.0953,2891458.0],[1637342700000,1.0951,1.0984,1.0945,1.0974,2600052.3999999999],[1637343000000,1.0973,1.0975,1.092,1.0943,1826843.7],[1637343300000,1.0942,1.0971,1.093,1.0951,2493674.7000000002],[1637343600000,1.0951,1.0952,1.0889,1.0921,3589679.0],[1637343900000,1.092,1.0934,1.0902,1.0906,2088592.0],[1637344200000,1.0907,1.0909,1.089,1.0908,1497654.0],[1637344500000,1.0907,1.0935,1.0903,1.0908,1551172.0],[1637344800000,1.0908,1.0919,1.0891,1.0908,1371659.8999999999],[1637345100000,1.0909,1.0923,1.0901,1.0912,1099405.2],[1637345400000,1.0912,1.0952,1.0912,1.0937,1544849.7],[1637345700000,1.0937,1.0939,1.0906,1.091,1028408.1],[1637346000000,1.0911,1.0919,1.088,1.0887,1541774.1000000001],[1637346300000,1.0887,1.0888,1.0861,1.0875,1889732.3999999999],[1637346600000,1.0876,1.088,1.0855,1.0879,1551550.0],[1637346900000,1.0879,1.0891,1.0869,1.0876,1318291.0],[1637347200000,1.0875,1.0946,1.0859,1.0929,4563689.9000000004],[1637347500000,1.0929,1.0939,1.0898,1.0915,1896728.6000000001],[1637347800000,1.0916,1.0923,1.0907,1.092,868193.3],[1637348100000,1.092,1.0921,1.0895,1.0911,881530.6],[1637348400000,1.091,1.0929,1.0904,1.0926,1430232.3999999999],[1637348700000,1.0925,1.0928,1.0909,1.0918,1058528.3999999999],[1637349000000,1.0918,1.0944,1.0904,1.0941,1438929.5],[1637349300000,1.094,1.0958,1.0933,1.0951,1214216.0],[1637349600000,1.095,1.0951,1.093,1.0937,1072236.0],[1637349900000,1.0936,1.0952,1.0926,1.0926,853420.7],[1637350200000,1.0926,1.093,1.0885,1.0906,2109807.7999999998],[1637350500000,1.0907,1.0916,1.0894,1.0903,777329.7],[1637350800000,1.0905,1.091,1.0881,1.0894,1155590.5],[1637351100000,1.0894,1.0928,1.0893,1.0909,1574971.2],[1637351400000,1.091,1.0941,1.0909,1.0924,1363640.2],[1637351700000,1.0924,1.0952,1.0917,1.0936,1362357.0],[1637352000000,1.0937,1.0949,1.0905,1.0916,1437675.3],[1637352300000,1.0915,1.0949,1.0909,1.0943,1157343.2],[1637352600000,1.0943,1.0955,1.0916,1.092,1653159.8],[1637352900000,1.0921,1.0927,1.0905,1.0918,945154.5],[1637353200000,1.0918,1.0946,1.0913,1.0926,1311882.8999999999],[1637353500000,1.0925,1.0927,1.086,1.0871,4312836.2000000002],[1637353800000,1.087,1.0885,1.0858,1.0877,1550918.6000000001],[1637354100000,1.0878,1.0878,1.0838,1.0859,2308438.5],[1637354400000,1.0859,1.0888,1.0853,1.0882,1855770.0],[1637354700000,1.0883,1.09,1.0867,1.0893,1192972.0],[1637355000000,1.0893,1.0894,1.0855,1.0864,1148445.5],[1637355300000,1.0866,1.0887,1.0855,1.0857,1099877.3999999999],[1637355600000,1.0856,1.0866,1.0837,1.0844,2039745.1000000001],[1637355900000,1.0844,1.0859,1.0801,1.0817,2892642.1000000001],[1637356200000,1.0816,1.0842,1.0813,1.083,2101040.1000000001],[1637356500000,1.0831,1.0854,1.0831,1.0851,1038172.5],[1637356800000,1.0851,1.0858,1.0836,1.0839,1119516.6000000001],[1637357100000,1.0839,1.0843,1.0817,1.0831,1538972.8],[1637357400000,1.0832,1.0856,1.0828,1.0854,1554488.0],[1637357700000,1.0854,1.0859,1.0832,1.0837,785075.1],[1637358000000,1.0838,1.0865,1.0837,1.0851,906393.9],[1637358300000,1.0851,1.0877,1.0847,1.0864,1111648.0],[1637358600000,1.0865,1.0872,1.0853,1.0862,662570.1],[1637358900000,1.0863,1.087,1.0845,1.087,651297.2],[1637359200000,1.087,1.089,1.0861,1.0863,1561620.3999999999],[1637359500000,1.0862,1.0897,1.0861,1.0896,690657.4],[1637359800000,1.0897,1.0906,1.0891,1.0893,1022069.4],[1637360100000,1.0893,1.0894,1.0872,1.0878,1387351.2],[1637360400000,1.0877,1.0885,1.087,1.0884,862012.8],[1637360700000,1.0885,1.0895,1.0879,1.089,647205.2],[1637361000000,1.089,1.0895,1.0876,1.0878,1066368.8],[1637361300000,1.0878,1.0881,1.0867,1.0879,858945.2],[1637361600000,1.0879,1.0879,1.0834,1.0847,1858069.5],[1637361900000,1.0848,1.0865,1.0845,1.0861,936997.9],[1637362200000,1.0861,1.0862,1.0825,1.0844,1495208.8999999999],[1637362500000,1.0844,1.0858,1.0829,1.0854,1352253.8],[1637362800000,1.0854,1.0871,1.0823,1.0849,1622230.3999999999],[1637363100000,1.0848,1.086,1.0828,1.0856,2417358.7999999998],[1637363400000,1.0855,1.088,1.0853,1.0867,1630532.2],[1637363700000,1.0867,1.0877,1.0848,1.085,760880.6],[1637364000000,1.085,1.0901,1.0848,1.0897,2069419.7],[1637364300000,1.0897,1.0911,1.0879,1.0879,1488516.5],[1637364600000,1.0879,1.0884,1.0856,1.0863,1533068.2],[1637364900000,1.0864,1.0875,1.0855,1.0874,722345.4],[1637365200000,1.0874,1.0874,1.0852,1.0857,822524.9],[1637365500000,1.0856,1.0902,1.0856,1.0902,1403417.8],[1637365800000,1.0902,1.0924,1.0901,1.0923,1636862.3999999999],[1637366100000,1.0923,1.0927,1.0895,1.0903,1399924.0],[1637366400000,1.0903,1.0962,1.0901,1.0914,4490673.2000000002],[1637366700000,1.0912,1.0941,1.0882,1.0941,3047626.5],[1637367000000,1.094,1.0977,1.0923,1.0971,3678381.5],[1637367300000,1.0972,1.1,1.0952,1.097,3997438.1000000001],[1637367600000,1.097,1.0977,1.0945,1.0972,2438163.0],[1637367900000,1.0972,1.0975,1.0956,1.0964,1098586.6000000001],[1637368200000,1.0964,1.0974,1.0897,1.0905,2539457.2999999998],[1637368500000,1.0905,1.0914,1.0865,1.0872,2320597.2000000002],[1637368800000,1.0872,1.0923,1.0864,1.0914,1631094.3],[1637369100000,1.0916,1.092,1.0877,1.0919,4289263.4000000004],[1637369400000,1.0919,1.094,1.091,1.0939,1055489.3],[1637369700000,1.0938,1.094,1.0901,1.0911,1184568.6000000001],[1637370000000,1.0912,1.0939,1.0903,1.0932,1212137.7],[1637370300000,1.0936,1.0955,1.0928,1.0951,1211542.5],[1637370600000,1.0952,1.0967,1.0945,1.096,1305328.2],[1637370900000,1.096,1.0961,1.0935,1.0947,1120283.7],[1637371200000,1.0947,1.0983,1.0939,1.0978,2445861.6000000001],[1637371500000,1.0979,1.0988,1.0969,1.098,1417139.0],[1637371800000,1.098,1.099,1.0967,1.098,1077261.6000000001],[1637372100000,1.0981,1.0986,1.0972,1.0981,674376.4],[1637372400000,1.0981,1.1005,1.0979,1.0983,2364323.5],[1637372700000,1.0982,1.0983,1.0967,1.0978,1247239.3999999999],[1637373000000,1.0978,1.099,1.0973,1.0981,1277629.1000000001],[1637373300000,1.0981,1.0999,1.0975,1.0985,1346729.3],[1637373600000,1.0985,1.0986,1.0944,1.0958,2723454.3999999999],[1637373900000,1.0958,1.0964,1.0929,1.093,1749144.7],[1637374200000,1.0929,1.0949,1.0914,1.0947,2049263.7],[1637374500000,1.0946,1.0956,1.0933,1.095,1205850.3],[1637374800000,1.095,1.0955,1.0898,1.0913,4338975.2000000002],[1637375100000,1.0913,1.0918,1.0894,1.0909,1560380.0],[1637375400000,1.0908,1.0918,1.0904,1.0906,972871.6],[1637375700000,1.0907,1.0913,1.0898,1.09,818448.0],[1637376000000,1.0899,1.0908,1.0894,1.0902,929097.9],[1637376300000,1.0902,1.0906,1.0891,1.0901,708380.7],[1637376600000,1.0901,1.0906,1.0897,1.0898,736159.1],[1637376900000,1.0898,1.0911,1.0896,1.0898,575795.6],[1637377200000,1.0897,1.0918,1.0892,1.0892,1190381.5],[1637377500000,1.0894,1.0921,1.0891,1.0908,765414.3],[1637377800000,1.0908,1.0908,1.0871,1.0878,1903675.5],[1637378100000,1.0879,1.0899,1.0876,1.0892,855392.0],[1637378400000,1.0892,1.0892,1.0864,1.0881,1257836.3999999999],[1637378700000,1.0882,1.0889,1.088,1.0883,536165.5],[1637379000000,1.0884,1.0899,1.088,1.0884,615970.0],[1637379300000,1.0885,1.0885,1.0861,1.0869,1734457.8],[1637379600000,1.0868,1.0871,1.0836,1.0867,2512820.2000000002],[1637379900000,1.0866,1.0868,1.0854,1.0858,859900.4],[1637380200000,1.0859,1.0868,1.0858,1.086,923713.7],[1637380500000,1.086,1.0865,1.0844,1.0856,858783.1],[1637380800000,1.0856,1.089,1.085,1.0859,3112801.1000000001],[1637381100000,1.0859,1.0867,1.0842,1.0845,893190.8],[1637381400000,1.0845,1.086,1.0844,1.0857,1771293.6000000001],[1637381700000,1.0857,1.087,1.0848,1.087,657313.5],[1637382000000,1.087,1.0874,1.0862,1.0871,500026.1],[1637382300000,1.0872,1.0875,1.0854,1.0861,611553.3],[1637382600000,1.086,1.0873,1.0856,1.0864,590790.3],[1637382900000,1.0865,1.0889,1.0864,1.0888,736771.6],[1637383200000,1.0887,1.0892,1.0875,1.0882,564684.6],[1637383500000,1.0883,1.0889,1.0867,1.0885,811903.8],[1637383800000,1.0884,1.0906,1.088,1.0888,1212980.8999999999],[1637384100000,1.0887,1.0893,1.0878,1.0882,464356.2],[1637384400000,1.0881,1.0901,1.0881,1.0899,669739.7],[1637384700000,1.0899,1.0899,1.0872,1.0883,533913.3],[1637385000000,1.0883,1.0896,1.0868,1.0868,2708301.2999999998],[1637385300000,1.0869,1.088,1.0843,1.0845,1120246.0],[1637385600000,1.0844,1.0857,1.0821,1.0855,1427580.0],[1637385900000,1.0854,1.0866,1.0851,1.0863,389398.7],[1637386200000,1.0862,1.0882,1.0855,1.0879,1024285.7],[1637386500000,1.0879,1.0883,1.0875,1.0876,497392.0],[1637386800000,1.0876,1.0878,1.0861,1.0873,511895.0],[1637387100000,1.0873,1.0874,1.0851,1.0856,1142755.2],[1637387400000,1.0855,1.0873,1.0852,1.0863,901715.7],[1637387700000,1.0862,1.0867,1.0842,1.0863,692900.3],[1637388000000,1.0864,1.0871,1.0847,1.0852,561651.4],[1637388300000,1.0853,1.0861,1.0843,1.0856,676985.3],[1637388600000,1.0855,1.0862,1.0848,1.0861,742870.5],[1637388900000,1.0862,1.0875,1.0854,1.0867,841786.8],[1637389200000,1.0868,1.092,1.0867,1.0915,2294600.3999999999],[1637389500000,1.0914,1.0917,1.0883,1.0893,1009314.8],[1637389800000,1.0894,1.0926,1.0892,1.0917,1368490.8999999999],[1637390100000,1.0916,1.0916,1.0886,1.0894,1084968.3],[1637390400000,1.0895,1.0896,1.0879,1.088,932017.3],[1637390700000,1.088,1.0883,1.0865,1.0871,767340.1],[1637391000000,1.0871,1.0897,1.0859,1.0889,1704183.8],[1637391300000,1.089,1.0897,1.0883,1.0892,455040.6],[1637391600000,1.0893,1.0895,1.0882,1.0884,393659.6],[1637391900000,1.0885,1.0895,1.0881,1.0893,567088.8],[1637392200000,1.0893,1.091,1.0892,1.0902,717566.6],[1637392500000,1.0903,1.0903,1.0883,1.0889,774337.1],[1637392800000,1.0888,1.0889,1.087,1.0871,449758.3],[1637393100000,1.0871,1.088,1.0863,1.087,933884.1],[1637393400000,1.0871,1.0879,1.0866,1.0866,596192.9],[1637393700000,1.0866,1.0886,1.086,1.0883,1050754.8],[1637394000000,1.0883,1.0886,1.0874,1.0881,509677.1],[1637394300000,1.0881,1.0882,1.0853,1.0863,1169936.7],[1637394600000,1.0863,1.0864,1.0847,1.0849,578612.8],[1637394900000,1.0849,1.086,1.0846,1.0856,1144627.1000000001],[1637395200000,1.0857,1.0866,1.085,1.0854,989275.4],[1637395500000,1.0855,1.0863,1.0779,1.0808,10505885.3000000007],[1637395800000,1.0808,1.0839,1.0807,1.0821,3208775.7000000002],[1637396100000,1.0821,1.0831,1.0806,1.083,1995404.0],[1637396400000,1.083,1.0846,1.0829,1.0841,681952.6],[1637396700000,1.084,1.0855,1.084,1.0845,794261.4],[1637397000000,1.0845,1.0862,1.0842,1.0855,888880.8],[1637397300000,1.0854,1.0864,1.0834,1.0862,1091271.2],[1637397600000,1.0863,1.0893,1.0858,1.0877,1279769.0],[1637397900000,1.0876,1.0881,1.0855,1.086,793110.0],[1637398200000,1.0859,1.0874,1.0859,1.0867,485470.1],[1637398500000,1.0866,1.0867,1.0837,1.0838,887626.4],[1637398800000,1.0838,1.0863,1.0838,1.0862,1245719.2],[1637399100000,1.0862,1.0884,1.0859,1.0883,974988.0],[1637399400000,1.0884,1.0888,1.0877,1.0886,1003591.0],[1637399700000,1.0885,1.0886,1.0861,1.0864,992947.0],[1637400000000,1.0864,1.0881,1.0863,1.087,779589.5],[1637400300000,1.087,1.0874,1.0844,1.0847,1269541.6000000001],[1637400600000,1.0846,1.0849,1.0831,1.0835,915033.7],[1637400900000,1.0835,1.0865,1.0834,1.0857,1087950.8999999999],[1637401200000,1.0857,1.0877,1.0856,1.0873,537262.9],[1637401500000,1.0873,1.0877,1.0862,1.0877,701827.7],[1637401800000,1.0876,1.0877,1.086,1.0863,755964.2],[1637402100000,1.0863,1.088,1.086,1.0864,926433.9],[1637402400000,1.0863,1.0864,1.084,1.0856,1366293.6000000001],[1637402700000,1.0855,1.0866,1.0843,1.086,796402.5],[1637403000000,1.086,1.0871,1.086,1.0868,381267.2],[1637403300000,1.0868,1.0887,1.0865,1.0879,1003275.7],[1637403600000,1.0878,1.0894,1.0877,1.0884,1017682.3],[1637403900000,1.0884,1.0896,1.0874,1.0895,879450.2],[1637404200000,1.0895,1.0907,1.0866,1.0875,1907528.3],[1637404500000,1.0874,1.0876,1.0861,1.0873,839201.7],[1637404800000,1.0872,1.089,1.0871,1.0878,682662.9],[1637405100000,1.0878,1.0886,1.0867,1.0868,559829.6],[1637405400000,1.0868,1.0871,1.0852,1.0865,1215875.8999999999],[1637405700000,1.0864,1.0879,1.0864,1.0873,937912.4],[1637406000000,1.0874,1.0892,1.0874,1.0878,944114.4],[1637406300000,1.0878,1.0903,1.0878,1.0902,979827.1],[1637406600000,1.0903,1.0904,1.0892,1.0898,805613.7],[1637406900000,1.0898,1.0901,1.0868,1.0881,1123460.1000000001],[1637407200000,1.0881,1.0904,1.0879,1.09,888838.0],[1637407500000,1.09,1.0905,1.0885,1.0897,750959.0],[1637407800000,1.0897,1.0901,1.0888,1.089,886774.2],[1637408100000,1.089,1.0969,1.089,1.0966,5718003.2999999998],[1637408400000,1.0967,1.0986,1.0956,1.0982,2973321.8999999999],[1637408700000,1.0982,1.0988,1.0968,1.0974,2032980.7],[1637409000000,1.0974,1.0976,1.0929,1.0966,2923037.3999999999],[1637409300000,1.0966,1.098,1.0954,1.0971,1428928.1000000001],[1637409600000,1.0971,1.1024,1.0967,1.1005,3556082.2999999998],[1637409900000,1.1004,1.1009,1.0937,1.095,3633304.7000000002],[1637410200000,1.095,1.0986,1.095,1.0969,2066445.8],[1637410500000,1.0968,1.0979,1.0949,1.0966,2075631.8999999999],[1637410800000,1.0965,1.0969,1.094,1.0948,1400437.5],[1637411100000,1.0949,1.0953,1.094,1.0947,723203.3],[1637411400000,1.0947,1.0964,1.0926,1.0946,1518842.8999999999],[1637411700000,1.0945,1.0951,1.0936,1.0938,702839.0],[1637412000000,1.0938,1.0938,1.091,1.0922,1820219.7],[1637412300000,1.0922,1.0929,1.09,1.0926,1242401.1000000001],[1637412600000,1.0926,1.0937,1.092,1.0932,945032.3],[1637412900000,1.0931,1.0941,1.0929,1.093,783040.7],[1637413200000,1.0931,1.0943,1.0928,1.0933,865950.4],[1637413500000,1.0933,1.0954,1.0928,1.0952,972369.5],[1637413800000,1.0952,1.0957,1.0934,1.0936,986854.5],[1637414100000,1.0936,1.0938,1.091,1.0915,1628016.7],[1637414400000,1.0916,1.0918,1.0886,1.0903,2689959.2000000002],[1637414700000,1.0902,1.0919,1.0901,1.091,873543.9],[1637415000000,1.091,1.0911,1.0896,1.09,1140434.0],[1637415300000,1.09,1.0901,1.0874,1.0877,1993566.8],[1637415600000,1.0877,1.0905,1.0874,1.0904,1408563.1000000001],[1637415900000,1.0904,1.0906,1.0877,1.088,1280882.3999999999],[1637416200000,1.088,1.0881,1.0862,1.0879,1721945.6000000001],[1637416500000,1.0879,1.0891,1.0875,1.088,878254.6],[1637416800000,1.0881,1.0903,1.0881,1.0884,1015559.4],[1637417100000,1.0883,1.0901,1.0867,1.0896,1573042.3],[1637417400000,1.0896,1.0915,1.0892,1.0901,1649532.8999999999],[1637417700000,1.0902,1.0921,1.0901,1.0918,780278.3],[1637418000000,1.0918,1.0929,1.0899,1.0909,1178830.8],[1637418300000,1.091,1.091,1.0871,1.0874,1673685.5],[1637418600000,1.0873,1.0878,1.0856,1.0876,1594742.1000000001],[1637418900000,1.0875,1.0903,1.0875,1.0882,1185215.8],[1637419200000,1.0882,1.0889,1.0873,1.0882,901834.1],[1637419500000,1.0881,1.0908,1.0881,1.0905,1190476.0],[1637419800000,1.0904,1.0909,1.088,1.0885,1333521.1000000001],[1637420100000,1.0885,1.0889,1.0868,1.0875,1204363.2],[1637420400000,1.0874,1.0882,1.0774,1.0774,6897278.2000000002],[1637420700000,1.0774,1.0798,1.0666,1.0734,18737028.1000000015],[1637421000000,1.0734,1.0744,1.0649,1.0653,8081248.0999999996],[1637421300000,1.0653,1.0713,1.0634,1.0682,6168610.9000000004],[1637421600000,1.0683,1.0738,1.0682,1.0723,3102912.2999999998],[1637421900000,1.0722,1.0724,1.0685,1.0696,3068162.8999999999],[1637422200000,1.0697,1.0742,1.0689,1.0726,3058278.2000000002],[1637422500000,1.0726,1.0727,1.068,1.0681,2896892.3999999999],[1637422800000,1.0682,1.069,1.06,1.0648,9484606.5],[1637423100000,1.0648,1.0683,1.0627,1.0658,6391215.2000000002],[1637423400000,1.0657,1.0662,1.0626,1.0636,2885611.7000000002],[1637423700000,1.0636,1.0687,1.0633,1.0657,3151402.8999999999],[1637424000000,1.0656,1.0731,1.0638,1.0703,7003005.7999999998],[1637424300000,1.0703,1.0714,1.0679,1.0682,2512787.7000000002],[1637424600000,1.0681,1.0698,1.067,1.0675,2116040.2999999998],[1637424900000,1.0675,1.0703,1.0622,1.0636,3607023.0],[1637425200000,1.0637,1.067,1.0619,1.0668,3870316.7999999998],[1637425500000,1.0667,1.0702,1.0661,1.0695,2504189.2000000002],[1637425800000,1.0695,1.0747,1.0684,1.0725,3386024.2000000002],[1637426100000,1.0724,1.0724,1.0703,1.0714,1934522.6000000001],[1637426400000,1.0715,1.0734,1.0709,1.0727,1171828.0],[1637426700000,1.0726,1.0744,1.0714,1.0737,1507495.8999999999],[1637427000000,1.0736,1.0737,1.0711,1.0714,1028025.1],[1637427300000,1.0715,1.0741,1.0714,1.0731,1073486.7],[1637427600000,1.0731,1.0749,1.0726,1.073,1826134.2],[1637427900000,1.073,1.0742,1.0712,1.0738,918052.8],[1637428200000,1.0738,1.0738,1.0719,1.0722,988718.0],[1637428500000,1.0723,1.0731,1.0703,1.0721,1311910.8],[1637428800000,1.0721,1.0723,1.0694,1.0699,1087906.5],[1637429100000,1.07,1.0711,1.0687,1.071,1325384.2],[1637429400000,1.071,1.0723,1.0698,1.0707,1655250.3],[1637429700000,1.0708,1.0709,1.0681,1.0691,1158388.3999999999],[1637430000000,1.0692,1.0713,1.0683,1.0711,1145710.2],[1637430300000,1.071,1.0734,1.0704,1.0705,1603998.2],[1637430600000,1.0705,1.0736,1.0704,1.0731,1422780.1000000001],[1637430900000,1.073,1.0733,1.0717,1.0729,804557.5],[1637431200000,1.0729,1.0733,1.0701,1.0711,2708513.8999999999],[1637431500000,1.0712,1.0724,1.0685,1.0696,2337731.0],[1637431800000,1.0696,1.0715,1.069,1.0712,1218239.6000000001],[1637432100000,1.0712,1.078,1.0709,1.077,3115678.5],[1637432400000,1.0771,1.0901,1.0765,1.089,10331195.0999999996],[1637432700000,1.089,1.0947,1.0885,1.0938,7902295.5999999996],[1637433000000,1.0939,1.0958,1.0874,1.0882,5300217.7999999998],[1637433300000,1.0882,1.0917,1.0865,1.0902,3014248.7000000002],[1637433600000,1.0901,1.0956,1.0895,1.0956,4186690.7999999998],[1637433900000,1.0956,1.0981,1.0931,1.0948,5519539.0999999996],[1637434200000,1.0948,1.0974,1.0927,1.0966,3683840.6000000001],[1637434500000,1.0965,1.0969,1.0928,1.094,1721444.3999999999],[1637434800000,1.0941,1.0959,1.092,1.0933,3065797.7999999998],[1637435100000,1.0933,1.0952,1.093,1.0933,2150903.7000000002],[1637435400000,1.0934,1.0955,1.0929,1.094,2857204.1000000001],[1637435700000,1.094,1.0953,1.0926,1.0932,2070352.5],[1637436000000,1.0933,1.0948,1.0924,1.0925,1837116.3999999999],[1637436300000,1.0925,1.0928,1.0894,1.0921,2155167.2000000002],[1637436600000,1.0921,1.0929,1.0906,1.0925,1278498.3],[1637436900000,1.0924,1.0939,1.0919,1.093,1043067.9],[1637437200000,1.0931,1.0933,1.0914,1.0921,912194.9],[1637437500000,1.0921,1.0921,1.0897,1.0907,1311876.8999999999],[1637437800000,1.0907,1.0913,1.0896,1.0897,1085619.1000000001],[1637438100000,1.0898,1.0913,1.0895,1.0913,1220068.3999999999],[1637438400000,1.0912,1.093,1.0901,1.0902,2883167.7000000002],[1637438700000,1.0902,1.0922,1.0901,1.0916,1082536.0],[1637439000000,1.0917,1.0922,1.0897,1.0898,1207431.3999999999],[1637439300000,1.0897,1.0908,1.0892,1.0894,818445.8],[1637439600000,1.0893,1.0904,1.0884,1.0902,963286.2],[1637439900000,1.0903,1.0915,1.0893,1.0902,1062387.6000000001],[1637440200000,1.0903,1.0909,1.089,1.0895,818111.2],[1637440500000,1.0895,1.0906,1.0894,1.0904,423345.4],[1637440800000,1.0905,1.0908,1.0899,1.0903,589835.3],[1637441100000,1.0904,1.091,1.0891,1.0908,1313067.3],[1637441400000,1.0908,1.0918,1.0905,1.0908,627179.8],[1637441700000,1.0909,1.0932,1.0908,1.0926,1093904.2],[1637442000000,1.0926,1.0927,1.09,1.0902,1134073.1000000001],[1637442300000,1.0902,1.0902,1.0886,1.0899,967459.1],[1637442600000,1.0899,1.0922,1.0897,1.0919,1200459.6000000001],[1637442900000,1.0919,1.0925,1.0902,1.0903,577084.6],[1637443200000,1.0904,1.0916,1.0901,1.0908,516103.0],[1637443500000,1.0907,1.0924,1.0907,1.0915,1033097.5],[1637443800000,1.0916,1.0937,1.0911,1.0924,1445591.8999999999],[1637444100000,1.0924,1.0939,1.0917,1.0939,730517.6],[1637444400000,1.0939,1.0949,1.0928,1.0934,1642349.3],[1637444700000,1.0934,1.0946,1.0934,1.0939,586339.8],[1637445000000,1.0939,1.095,1.0927,1.0927,597216.7],[1637445300000,1.0927,1.0936,1.0922,1.0929,1007741.2],[1637445600000,1.0929,1.0946,1.0927,1.0935,868797.3],[1637445900000,1.0937,1.0938,1.0916,1.0926,802146.7],[1637446200000,1.0925,1.0934,1.0916,1.0919,857952.7],[1637446500000,1.0919,1.0939,1.0919,1.0935,438778.0],[1637446800000,1.0935,1.0938,1.0917,1.0935,1225414.7],[1637447100000,1.0936,1.0947,1.0935,1.0939,586802.1],[1637447400000,1.0939,1.0941,1.0936,1.0941,349812.6],[1637447700000,1.094,1.0948,1.0936,1.0945,664782.5],[1637448000000,1.0946,1.0963,1.0945,1.0958,1088906.8999999999],[1637448300000,1.0958,1.0966,1.0951,1.0953,876719.8],[1637448600000,1.0953,1.0954,1.0945,1.0948,509822.1],[1637448900000,1.0948,1.0969,1.0942,1.0961,1505118.8999999999],[1637449200000,1.096,1.097,1.0957,1.0965,1148066.1000000001],[1637449500000,1.0966,1.098,1.0948,1.0976,1567379.7],[1637449800000,1.0976,1.098,1.095,1.0956,998647.3],[1637450100000,1.0956,1.0969,1.0952,1.0953,750595.9],[1637450400000,1.0954,1.0956,1.0927,1.0935,1621045.8],[1637450700000,1.0935,1.0952,1.0928,1.0937,925914.1],[1637451000000,1.0937,1.0961,1.0937,1.0953,695201.5],[1637451300000,1.0954,1.0972,1.0953,1.0968,676331.4],[1637451600000,1.0969,1.0977,1.0961,1.097,770746.1],[1637451900000,1.0971,1.098,1.0962,1.0971,784673.1],[1637452200000,1.097,1.0987,1.0961,1.0978,1375433.6000000001],[1637452500000,1.0978,1.0978,1.0956,1.0976,1412024.6000000001],[1637452800000,1.0975,1.0988,1.0951,1.0962,1990264.8999999999],[1637453100000,1.0962,1.0969,1.0946,1.095,780066.8],[1637453400000,1.095,1.0977,1.0937,1.0975,1258294.2],[1637453700000,1.0975,1.0981,1.0924,1.0931,1243792.3],[1637454000000,1.093,1.0956,1.0922,1.094,1261851.6000000001],[1637454300000,1.094,1.0941,1.0915,1.0924,999709.6],[1637454600000,1.0924,1.0926,1.0906,1.0917,1183609.0],[1637454900000,1.0917,1.0926,1.0906,1.0925,997068.5],[1637455200000,1.0924,1.094,1.0923,1.0929,969486.3],[1637455500000,1.0929,1.0935,1.0911,1.0917,781625.8],[1637455800000,1.0917,1.0924,1.0903,1.0912,721801.4],[1637456100000,1.0912,1.0918,1.0901,1.0903,1098562.5],[1637456400000,1.0903,1.0935,1.0903,1.0924,1184347.7],[1637456700000,1.0924,1.0927,1.0911,1.0911,581214.6],[1637457000000,1.0911,1.0932,1.0911,1.0926,726532.9],[1637457300000,1.0925,1.093,1.0911,1.0911,719980.4],[1637457600000,1.0911,1.0918,1.0904,1.0915,453159.2],[1637457900000,1.0914,1.0918,1.0909,1.0909,370430.5],[1637458200000,1.091,1.0911,1.0886,1.09,2121856.8999999999],[1637458500000,1.09,1.0915,1.0898,1.0904,700277.1],[1637458800000,1.0903,1.0907,1.0891,1.0903,473211.3],[1637459100000,1.0903,1.0921,1.087,1.0874,1230450.3],[1637459400000,1.0875,1.0888,1.086,1.0875,1717497.0],[1637459700000,1.0875,1.0894,1.0873,1.0888,784000.5],[1637460000000,1.0888,1.0894,1.0875,1.0891,767362.7],[1637460300000,1.0891,1.0898,1.0882,1.0889,409101.5],[1637460600000,1.089,1.089,1.0863,1.0865,799687.8],[1637460900000,1.0865,1.0875,1.0864,1.0875,478476.7],[1637461200000,1.0874,1.0876,1.0848,1.0852,1712651.7],[1637461500000,1.0852,1.0864,1.0833,1.0835,1532113.7],[1637461800000,1.0835,1.085,1.0783,1.0805,5003637.0999999996],[1637462100000,1.0805,1.0815,1.0732,1.0741,5105530.0],[1637462400000,1.0741,1.0777,1.074,1.0766,2968626.1000000001],[1637462700000,1.0766,1.0789,1.0764,1.0787,1551175.6000000001],[1637463000000,1.0788,1.0791,1.0777,1.0783,1160593.5],[1637463300000,1.0783,1.0811,1.0763,1.0802,1934711.2],[1637463600000,1.0803,1.0809,1.0774,1.0787,1737260.7],[1637463900000,1.0787,1.0824,1.0786,1.0811,1311801.0],[1637464200000,1.081,1.0835,1.0808,1.0819,1937098.3],[1637464500000,1.0819,1.0819,1.0793,1.0804,1190680.3999999999],[1637464800000,1.0803,1.0817,1.0795,1.0795,1084963.5],[1637465100000,1.0795,1.0809,1.0783,1.0796,1058532.3999999999],[1637465400000,1.0796,1.0803,1.0787,1.0791,653388.2],[1637465700000,1.0792,1.0808,1.0786,1.0786,934467.7],[1637466000000,1.0786,1.079,1.0771,1.0773,922674.1],[1637466300000,1.0773,1.078,1.0765,1.0775,748529.9],[1637466600000,1.0775,1.0781,1.0743,1.0776,1872877.2],[1637466900000,1.0776,1.0779,1.0751,1.077,910629.8],[1637467200000,1.0769,1.0791,1.0751,1.0791,1420191.3],[1637467500000,1.0791,1.0798,1.0777,1.0792,1197974.1000000001],[1637467800000,1.0792,1.0819,1.0792,1.0802,1130051.8],[1637468100000,1.0802,1.0825,1.0794,1.0803,1201338.2],[1637468400000,1.0803,1.0814,1.0799,1.0803,480717.8],[1637468700000,1.0804,1.081,1.0788,1.0788,1022164.1],[1637469000000,1.0788,1.0803,1.0783,1.0801,758983.6],[1637469300000,1.08,1.08,1.0775,1.0786,825612.3],[1637469600000,1.0785,1.0788,1.0775,1.0786,511487.8],[1637469900000,1.0786,1.0787,1.0767,1.0775,987343.4],[1637470200000,1.0776,1.0777,1.0759,1.0767,718581.7],[1637470500000,1.0767,1.0773,1.0758,1.0767,547054.9],[1637470800000,1.0768,1.078,1.0758,1.0771,547032.8],[1637471100000,1.0772,1.079,1.0765,1.077,660939.3],[1637471400000,1.0769,1.0786,1.0766,1.078,856206.1],[1637471700000,1.078,1.0792,1.0776,1.0777,476814.3],[1637472000000,1.0777,1.0786,1.0777,1.0781,335511.9],[1637472300000,1.078,1.079,1.0777,1.0784,391906.0],[1637472600000,1.0785,1.0823,1.0782,1.0821,1189955.7],[1637472900000,1.082,1.0825,1.081,1.0821,876731.4],[1637473200000,1.0822,1.0832,1.0809,1.0823,696226.2],[1637473500000,1.0822,1.0838,1.0819,1.0832,853775.1],[1637473800000,1.0832,1.0843,1.0832,1.0841,696519.2],[1637474100000,1.0841,1.0857,1.0825,1.0853,1808459.5],[1637474400000,1.0853,1.0854,1.0835,1.0837,1168583.8],[1637474700000,1.0837,1.0839,1.0821,1.0832,783244.2],[1637475000000,1.0832,1.0833,1.0821,1.0828,452212.4],[1637475300000,1.0828,1.0838,1.0825,1.0833,301469.1],[1637475600000,1.0834,1.0835,1.0812,1.0814,464090.7],[1637475900000,1.0815,1.0815,1.0802,1.0805,767040.1],[1637476200000,1.0805,1.0843,1.0805,1.0838,883357.9],[1637476500000,1.0838,1.0839,1.0804,1.0808,579035.1],[1637476800000,1.0808,1.0819,1.0795,1.0806,797439.9],[1637477100000,1.0805,1.0825,1.0801,1.0823,825426.5],[1637477400000,1.0822,1.0825,1.081,1.0812,547369.3],[1637477700000,1.0813,1.085,1.0812,1.0847,1108606.1000000001],[1637478000000,1.0847,1.0903,1.0831,1.0831,4807307.0],[1637478300000,1.0831,1.0847,1.0828,1.0843,650309.2],[1637478600000,1.0843,1.0844,1.0829,1.0834,771278.2],[1637478900000,1.0835,1.0835,1.0811,1.0824,793377.3],[1637479200000,1.0824,1.084,1.0822,1.0828,513116.3],[1637479500000,1.0828,1.0842,1.0821,1.0827,919236.9],[1637479800000,1.0827,1.0847,1.0827,1.0843,549662.5],[1637480100000,1.0842,1.0845,1.0825,1.0829,707142.8],[1637480400000,1.0829,1.0839,1.0818,1.0831,522142.3],[1637480700000,1.0832,1.0837,1.0813,1.0813,552156.2],[1637481000000,1.0813,1.0821,1.081,1.0813,532391.4],[1637481300000,1.0812,1.0813,1.0795,1.0803,834204.0],[1637481600000,1.0804,1.0813,1.079,1.0793,978148.0],[1637481900000,1.0793,1.0797,1.0765,1.077,1781373.8999999999],[1637482200000,1.077,1.0782,1.0742,1.0778,2554242.6000000001],[1637482500000,1.0778,1.0781,1.0743,1.075,1286432.1000000001],[1637482800000,1.0749,1.0774,1.0742,1.0769,1188115.7],[1637483100000,1.0769,1.0776,1.0744,1.0758,1387133.6000000001],[1637483400000,1.0759,1.0776,1.0748,1.0767,1571797.6000000001],[1637483700000,1.0768,1.0796,1.0767,1.0787,1558711.2],[1637484000000,1.0787,1.079,1.077,1.0776,1283710.6000000001],[1637484300000,1.0776,1.0776,1.0741,1.0742,1409473.3999999999],[1637484600000,1.0741,1.0753,1.0705,1.071,2226765.7999999998],[1637484900000,1.071,1.0738,1.07,1.0723,2707738.7000000002],[1637485200000,1.0722,1.0743,1.0711,1.074,1182262.2],[1637485500000,1.0741,1.0745,1.0728,1.0736,788009.3],[1637485800000,1.0736,1.0761,1.0733,1.0749,1058760.1000000001],[1637486100000,1.0749,1.0759,1.0745,1.0752,1119925.5],[1637486400000,1.0751,1.0762,1.0742,1.0749,910704.1],[1637486700000,1.0749,1.0765,1.0737,1.0738,1493216.8999999999],[1637487000000,1.0738,1.0756,1.0729,1.0741,1005417.0],[1637487300000,1.0741,1.0755,1.0728,1.0753,829939.0],[1637487600000,1.0752,1.0765,1.074,1.0744,809630.2],[1637487900000,1.0744,1.0759,1.0738,1.0746,717923.0],[1637488200000,1.0746,1.0765,1.0745,1.0756,820078.8],[1637488500000,1.0756,1.0783,1.0747,1.0779,1040966.2],[1637488800000,1.0779,1.0786,1.0775,1.0778,1053125.2],[1637489100000,1.0779,1.0783,1.0769,1.0779,811140.6],[1637489400000,1.0778,1.0779,1.0758,1.0762,650018.8],[1637489700000,1.0761,1.0793,1.0758,1.0788,1671629.0],[1637490000000,1.0788,1.0788,1.0768,1.0772,866999.2],[1637490300000,1.0773,1.0789,1.0768,1.0768,732410.1],[1637490600000,1.0768,1.0771,1.0737,1.0751,1686388.0],[1637490900000,1.075,1.0755,1.074,1.074,631987.9],[1637491200000,1.074,1.0743,1.0701,1.0724,2901798.5],[1637491500000,1.0724,1.0754,1.0715,1.0747,1143065.3],[1637491800000,1.0746,1.0766,1.0744,1.076,1246851.5],[1637492100000,1.076,1.0762,1.0745,1.0756,800906.1],[1637492400000,1.0756,1.0809,1.0745,1.0783,4955742.4000000004],[1637492700000,1.0782,1.0809,1.0774,1.079,2248110.1000000001],[1637493000000,1.079,1.0792,1.0767,1.0769,1823132.5],[1637493300000,1.077,1.0771,1.0708,1.0715,3368298.1000000001],[1637493600000,1.0715,1.0716,1.0652,1.071,6452206.2999999998],[1637493900000,1.0709,1.0711,1.0678,1.0694,1547093.3],[1637494200000,1.0694,1.0719,1.0685,1.0713,1943760.0],[1637494500000,1.0712,1.073,1.0704,1.0714,995341.1],[1637494800000,1.0714,1.0715,1.069,1.0714,1160709.3],[1637495100000,1.0714,1.073,1.0709,1.0721,1085871.3999999999],[1637495400000,1.0721,1.0731,1.0716,1.0723,958377.4],[1637495700000,1.0724,1.0734,1.0722,1.0726,442414.5],[1637496000000,1.0727,1.0727,1.07,1.0713,1616334.3999999999],[1637496300000,1.0713,1.0721,1.0711,1.0721,590388.2],[1637496600000,1.0721,1.0765,1.0721,1.0759,2206257.2000000002],[1637496900000,1.0759,1.0776,1.0743,1.0759,1760184.6000000001],[1637497200000,1.0759,1.0767,1.0749,1.0753,997043.0],[1637497500000,1.0754,1.0763,1.0748,1.0759,945988.8],[1637497800000,1.0759,1.0771,1.0737,1.074,1312225.7],[1637498100000,1.0741,1.0745,1.0724,1.0733,896465.1],[1637498400000,1.0734,1.0734,1.0714,1.0731,1280526.7],[1637498700000,1.073,1.0752,1.0727,1.0729,1084723.7],[1637499000000,1.073,1.0755,1.0729,1.0752,948844.9],[1637499300000,1.0751,1.0752,1.0732,1.0736,761256.9],[1637499600000,1.0736,1.0745,1.0728,1.073,724334.7],[1637499900000,1.0729,1.074,1.0724,1.0725,410581.6],[1637500200000,1.0725,1.0738,1.0714,1.0723,825709.7],[1637500500000,1.0724,1.0741,1.0722,1.0731,541109.7],[1637500800000,1.0731,1.0741,1.0729,1.0736,602113.7],[1637501100000,1.0736,1.0738,1.0718,1.072,772796.4],[1637501400000,1.072,1.0728,1.0706,1.0707,639585.1],[1637501700000,1.0708,1.0712,1.0672,1.0699,3931510.2999999998],[1637502000000,1.0699,1.0701,1.0681,1.0694,846705.8],[1637502300000,1.0693,1.0715,1.0688,1.0702,1041782.8],[1637502600000,1.0702,1.071,1.0685,1.069,1044235.0],[1637502900000,1.069,1.0695,1.0663,1.0678,2842689.8999999999],[1637503200000,1.0678,1.0679,1.0638,1.0676,3331142.2000000002],[1637503500000,1.0677,1.0677,1.0658,1.0661,1509472.6000000001],[1637503800000,1.066,1.0694,1.066,1.0686,1594643.8999999999],[1637504100000,1.0686,1.0694,1.0671,1.0673,934328.6],[1637504400000,1.0673,1.0698,1.0673,1.0696,907421.8],[1637504700000,1.0697,1.0698,1.0678,1.0685,700061.7],[1637505000000,1.0685,1.0712,1.0669,1.0709,1578725.6000000001],[1637505300000,1.0709,1.0729,1.0705,1.0724,3502201.0],[1637505600000,1.0724,1.074,1.0716,1.0733,1390219.3],[1637505900000,1.0735,1.0745,1.0717,1.0722,1348645.0],[1637506200000,1.0721,1.0741,1.0711,1.0736,1267274.8999999999],[1637506500000,1.0736,1.0741,1.07,1.0701,2498196.3999999999],[1637506800000,1.07,1.0705,1.0685,1.0697,1205914.8999999999],[1637507100000,1.0696,1.0728,1.0696,1.0728,996615.6],[1637507400000,1.0728,1.0728,1.0704,1.0713,863754.8],[1637507700000,1.0713,1.0734,1.0709,1.0734,1053392.6000000001],[1637508000000,1.0734,1.074,1.0723,1.074,733229.2],[1637508300000,1.0739,1.0794,1.0739,1.079,3367813.3999999999],[1637508600000,1.079,1.0793,1.0768,1.0782,1781674.3999999999],[1637508900000,1.0783,1.0788,1.0775,1.0783,1179503.3999999999],[1637509200000,1.0783,1.0818,1.0782,1.08,2515741.7999999998],[1637509500000,1.0801,1.0805,1.0786,1.0795,1321868.2],[1637509800000,1.0795,1.0807,1.0791,1.0801,1287931.0],[1637510100000,1.0801,1.0804,1.0787,1.0788,1565686.1000000001],[1637510400000,1.0787,1.0814,1.0786,1.0805,1763473.7],[1637510700000,1.0806,1.0838,1.0805,1.0823,2995209.7999999998],[1637511000000,1.0824,1.0826,1.0807,1.0809,1521309.6000000001],[1637511300000,1.081,1.0812,1.0772,1.0793,1840075.8],[1637511600000,1.0792,1.0798,1.0765,1.0776,1690043.6000000001],[1637511900000,1.0775,1.078,1.0754,1.0755,1063176.8999999999],[1637512200000,1.0756,1.077,1.0754,1.0769,1187972.8],[1637512500000,1.0769,1.0769,1.0749,1.0762,1593978.7],[1637512800000,1.0763,1.079,1.0762,1.0786,1059333.8999999999],[1637513100000,1.0786,1.083,1.0784,1.0823,3005583.8999999999],[1637513400000,1.0823,1.0853,1.0811,1.0848,3877599.2999999998],[1637513700000,1.0849,1.0864,1.0839,1.0854,3246257.0],[1637514000000,1.0854,1.0867,1.0818,1.0824,5822200.0999999996],[1637514300000,1.0825,1.084,1.08,1.0817,3095709.1000000001],[1637514600000,1.0818,1.0839,1.0813,1.0828,1452589.8],[1637514900000,1.0829,1.0847,1.0824,1.084,973392.1],[1637515200000,1.0841,1.0855,1.0832,1.0842,1350230.5],[1637515500000,1.0843,1.0855,1.0839,1.0839,1418790.7],[1637515800000,1.084,1.0849,1.082,1.0831,1278012.6000000001],[1637516100000,1.083,1.0845,1.0809,1.0812,1221282.8999999999],[1637516400000,1.0811,1.0828,1.081,1.0814,946701.2],[1637516700000,1.0813,1.0816,1.0799,1.08,1185232.1000000001],[1637517000000,1.0801,1.0811,1.0797,1.0805,814544.5],[1637517300000,1.0804,1.081,1.0797,1.0802,743002.3],[1637517600000,1.0804,1.081,1.0783,1.0784,1149454.1000000001],[1637517900000,1.0783,1.0805,1.0771,1.0796,1361561.5],[1637518200000,1.0797,1.0807,1.0791,1.0806,835282.1],[1637518500000,1.0807,1.0809,1.0793,1.0799,712053.8],[1637518800000,1.0799,1.0808,1.0778,1.0779,664907.9],[1637519100000,1.0779,1.0792,1.0765,1.0779,1317777.8],[1637519400000,1.078,1.0784,1.077,1.0783,414788.8],[1637519700000,1.0783,1.0799,1.0783,1.0786,447598.1],[1637520000000,1.0786,1.0794,1.0785,1.0792,348289.5],[1637520300000,1.0791,1.081,1.0781,1.0785,1278199.5],[1637520600000,1.0786,1.0802,1.0785,1.0801,636711.5],[1637520900000,1.0801,1.0808,1.0789,1.0801,701731.7],[1637521200000,1.0801,1.0817,1.08,1.0813,845379.6],[1637521500000,1.0812,1.0815,1.0778,1.0778,887534.6],[1637521800000,1.0777,1.0792,1.0775,1.0776,598421.4],[1637522100000,1.0776,1.0786,1.0744,1.0748,2436814.7000000002],[1637522400000,1.0748,1.0755,1.0627,1.0673,11657709.5999999996],[1637522700000,1.0674,1.0714,1.0674,1.0711,2419596.8999999999],[1637523000000,1.0712,1.0724,1.0709,1.0711,1822021.5],[1637523300000,1.071,1.0717,1.0692,1.0705,1204982.3999999999],[1637523600000,1.0706,1.0709,1.0688,1.0701,947123.0],[1637523900000,1.0701,1.0722,1.0678,1.0681,1683728.5],[1637524200000,1.0682,1.0691,1.0661,1.0682,1654263.7],[1637524500000,1.0683,1.07,1.0675,1.068,814100.6],[1637524800000,1.0681,1.071,1.068,1.0697,1127817.3],[1637525100000,1.0696,1.0714,1.0691,1.0708,1297767.2],[1637525400000,1.0707,1.0711,1.0695,1.0699,880530.2],[1637525700000,1.07,1.071,1.0696,1.0706,639251.1],[1637526000000,1.0706,1.0722,1.07,1.0711,1272377.8999999999],[1637526300000,1.071,1.0715,1.07,1.0715,766472.4],[1637526600000,1.0714,1.0723,1.0704,1.0719,912265.7],[1637526900000,1.0719,1.0742,1.0719,1.0727,1148654.3999999999],[1637527200000,1.0728,1.0741,1.0726,1.0734,1033655.7],[1637527500000,1.0734,1.0739,1.0727,1.0735,580060.4],[1637527800000,1.0734,1.0764,1.0734,1.0759,956301.3],[1637528100000,1.0759,1.0774,1.0755,1.076,1718106.0],[1637528400000,1.0759,1.0772,1.0743,1.0746,1695191.3999999999],[1637528700000,1.0747,1.076,1.0744,1.0753,754169.2],[1637529000000,1.0753,1.0753,1.0728,1.074,1454014.0],[1637529300000,1.074,1.0761,1.0738,1.0754,789771.2],[1637529600000,1.0753,1.077,1.0751,1.0768,708803.5],[1637529900000,1.0767,1.0781,1.0761,1.0775,953198.1],[1637530200000,1.0775,1.0784,1.0765,1.0771,1015064.0],[1637530500000,1.077,1.0777,1.0744,1.0751,1236336.5],[1637530800000,1.0752,1.0776,1.0751,1.0773,596424.3],[1637531100000,1.0773,1.0773,1.0753,1.0755,883756.5],[1637531400000,1.0755,1.0763,1.0736,1.0739,1046835.3],[1637531700000,1.0739,1.0753,1.0738,1.0752,485188.2],[1637532000000,1.0751,1.0759,1.073,1.074,1138527.0],[1637532300000,1.0741,1.0744,1.0732,1.0735,749520.8],[1637532600000,1.0736,1.0756,1.0735,1.0752,763636.5],[1637532900000,1.0753,1.0762,1.0753,1.0758,369235.1],[1637533200000,1.0759,1.0762,1.0736,1.0742,820142.4],[1637533500000,1.0742,1.0745,1.0721,1.0734,876413.1],[1637533800000,1.0733,1.0737,1.0711,1.0713,765844.7]]
\ No newline at end of file
diff --git a/tests/testdata/futures/XRP_USDT-8h-funding_rate.json b/tests/testdata/futures/XRP_USDT_USDT-8h-funding_rate.json
similarity index 100%
rename from tests/testdata/futures/XRP_USDT-8h-funding_rate.json
rename to tests/testdata/futures/XRP_USDT_USDT-8h-funding_rate.json
diff --git a/tests/testdata/futures/XRP_USDT-8h-mark.json b/tests/testdata/futures/XRP_USDT_USDT-8h-mark.json
similarity index 100%
rename from tests/testdata/futures/XRP_USDT-8h-mark.json
rename to tests/testdata/futures/XRP_USDT_USDT-8h-mark.json
diff --git a/tests/exchange/test_ccxt_precise.py b/tests/utils/test_ccxt_precise.py
similarity index 100%
rename from tests/exchange/test_ccxt_precise.py
rename to tests/utils/test_ccxt_precise.py
diff --git a/tests/utils/test_datetime_helpers.py b/tests/utils/test_datetime_helpers.py
new file mode 100644
index 000000000..5aec0da54
--- /dev/null
+++ b/tests/utils/test_datetime_helpers.py
@@ -0,0 +1,59 @@
+from datetime import datetime, timedelta, timezone
+
+import pytest
+import time_machine
+
+from freqtrade.util import dt_floor_day, dt_from_ts, dt_now, dt_ts, dt_utc, shorten_date
+from freqtrade.util.datetime_helpers import dt_humanize
+
+
+def test_dt_now():
+    with time_machine.travel("2021-09-01 05:01:00 +00:00", tick=False) as t:
+        now = datetime.now(timezone.utc)
+        assert dt_now() == now
+        assert dt_ts() == int(now.timestamp() * 1000)
+        assert dt_ts(now) == int(now.timestamp() * 1000)
+
+        t.shift(timedelta(hours=5))
+        assert dt_now() >= now
+        assert dt_now() == datetime.now(timezone.utc)
+        assert dt_ts() == int(dt_now().timestamp() * 1000)
+        # Test with different time than now
+        assert dt_ts(now) == int(now.timestamp() * 1000)
+
+
+def test_dt_utc():
+    assert dt_utc(2023, 5, 5) == datetime(2023, 5, 5, tzinfo=timezone.utc)
+    assert dt_utc(2023, 5, 5, 0, 0, 0, 555500) == datetime(2023, 5, 5, 0, 0, 0, 555500,
+                                                           tzinfo=timezone.utc)
+
+
+@pytest.mark.parametrize('as_ms', [True, False])
+def test_dt_from_ts(as_ms):
+    multi = 1000 if as_ms else 1
+    assert dt_from_ts(1683244800.0 * multi) == datetime(2023, 5, 5, tzinfo=timezone.utc)
+    assert dt_from_ts(1683244800.5555 * multi) == datetime(2023, 5, 5, 0, 0, 0, 555500,
+                                                           tzinfo=timezone.utc)
+    # As int
+    assert dt_from_ts(1683244800 * multi) == datetime(2023, 5, 5, tzinfo=timezone.utc)
+    # As milliseconds
+    assert dt_from_ts(1683244800 * multi) == datetime(2023, 5, 5, tzinfo=timezone.utc)
+    assert dt_from_ts(1683242400 * multi) == datetime(2023, 5, 4, 23, 20, tzinfo=timezone.utc)
+
+
+def test_dt_floor_day():
+    now = datetime(2023, 9, 1, 5, 2, 3, 455555, tzinfo=timezone.utc)
+
+    assert dt_floor_day(now) == datetime(2023, 9, 1, tzinfo=timezone.utc)
+
+
+def test_shorten_date() -> None:
+    str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
+    str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
+    assert shorten_date(str_data) == str_shorten_data
+
+
+def test_dt_humanize() -> None:
+    assert dt_humanize(dt_now()) == 'just now'
+    assert dt_humanize(dt_now(), only_distance=True) == 'instantly'
+    assert dt_humanize(dt_now() - timedelta(hours=16), only_distance=True) == '16 hours'
diff --git a/tests/test_periodiccache.py b/tests/utils/test_periodiccache.py
similarity index 100%
rename from tests/test_periodiccache.py
rename to tests/utils/test_periodiccache.py