diff --git a/.github/workflows/binance-lev-tier-update.yml b/.github/workflows/binance-lev-tier-update.yml new file mode 100644 index 000000000..f06251cca --- /dev/null +++ b/.github/workflows/binance-lev-tier-update.yml @@ -0,0 +1,47 @@ +name: Binance Leverage tiers update + +on: + schedule: + - cron: "0 3 * * 4" + # on demand + workflow_dispatch: + +permissions: + contents: read + +jobs: + auto-update: + runs-on: ubuntu-latest + environment: + name: develop + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install ccxt + run: pip install ccxt + + - name: Run leverage tier update + env: + CI_WEB_PROXY: ${{ secrets.CI_WEB_PROXY }} + FREQTRADE__EXCHANGE__KEY: ${{ secrets.BINANCE_EXCHANGE_KEY }} + FREQTRADE__EXCHANGE__SECRET: ${{ secrets.BINANCE_EXCHANGE_SECRET }} + run: python build_helpers/binance_update_lev_tiers.py + + + - uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.REPO_SCOPED_TOKEN }} + add-paths: freqtrade/exchange/binance_leverage_tiers.json + labels: | + Tech maintenance + Dependencies + branch: update/binance-leverage-tiers + title: Update Binance Leverage Tiers + commit-message: "chore: update pre-commit hooks" + committer: Freqtrade Bot + body: Update binance leverage tiers. + delete-branch: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba55eed04..caec64749 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: types: [published] pull_request: schedule: - - cron: '0 5 * * 4' + - cron: '0 3 * * 4' concurrency: group: "${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}" @@ -19,7 +19,7 @@ concurrency: permissions: repository-projects: read jobs: - build_linux: + build-linux: runs-on: ${{ matrix.os }} strategy: @@ -60,11 +60,16 @@ jobs: export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include pip install -r requirements-dev.txt + pip install -e ft_client/ pip install -e . + - name: Check for version alignment + run: | + python build_helpers/freqtrade_client_version_align.py + - name: Tests run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc + pytest --random-order --cov=freqtrade --cov=freqtrade_client --cov-config=.coveragerc - name: Coveralls if: (runner.os == 'Linux' && matrix.python-version == '3.10' && matrix.os == 'ubuntu-22.04') @@ -124,8 +129,11 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ "macos-latest", "macos-13" ] + os: [ "macos-latest", "macos-13", "macos-14" ] python-version: ["3.9", "3.10", "3.11", "3.12"] + exclude: + - os: "macos-14" + python-version: "3.9" steps: - uses: actions/checkout@v4 @@ -154,7 +162,7 @@ jobs: run: | cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. - - name: Installation - macOS + - name: Installation - macOS (Brew) run: | # brew update # TODO: Should be the brew upgrade @@ -177,11 +185,15 @@ jobs: rm /usr/local/bin/python3.12-config || true brew install hdf5 c-blosc libomp + + - name: Installation (python) + run: | python -m pip install --upgrade pip wheel export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include pip install -r requirements-dev.txt + pip install -e ft_client/ pip install -e . - name: Tests @@ -356,7 +368,7 @@ jobs: webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} - build_linux_online: + build-linux-online: # Run pytest with "live" checks runs-on: ubuntu-22.04 steps: @@ -392,29 +404,30 @@ jobs: export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include pip install -r requirements-dev.txt + pip install -e ft_client/ pip install -e . - name: Tests incl. ccxt compatibility tests env: CI_WEB_PROXY: http://152.67.78.211:13128 run: | - pytest --random-order --longrun --durations 20 -n auto --dist loadscope + pytest --random-order --longrun --durations 20 -n auto # Notify only once - when CI completes (and after deploy) in case it's successfull notify-complete: needs: [ - build_linux, + build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit, - build_linux_online + build-linux-online ] runs-on: ubuntu-22.04 # Discord notification can't handle schedule events - if: (github.event_name != 'schedule') + if: github.event_name != 'schedule' && github.repository == 'freqtrade/freqtrade' permissions: repository-projects: read steps: @@ -437,7 +450,7 @@ jobs: build: name: "Build" - needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ] + needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ] runs-on: ubuntu-22.04 steps: @@ -461,6 +474,19 @@ jobs: dist retention-days: 10 + - name: Build Client distribution + run: | + pip install -U build + python -m build --sdist --wheel ft_client + + - name: Upload artifacts 📦 + uses: actions/upload-artifact@v4 + with: + name: freqtrade-client-build + path: | + ft_client/dist + retention-days: 10 + deploy-pypi: name: "Deploy to PyPI" needs: [ build ] @@ -478,20 +504,22 @@ jobs: - name: Download artifact 📦 uses: actions/download-artifact@v4 with: - name: freqtrade-build + name: freqtrade*-build path: dist + merge-multiple: true + - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.8.14 with: repository-url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.8.14 deploy-docker: - needs: [ build_linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ] + needs: [ build-linux, build-macos, build-windows, docs-check, mypy-version-check, pre-commit ] runs-on: ubuntu-22.04 if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker-update-readme.yml similarity index 58% rename from .github/workflows/docker_update_readme.yml rename to .github/workflows/docker-update-readme.yml index d91e79099..95ab5b6ca 100644 --- a/.github/workflows/docker_update_readme.yml +++ b/.github/workflows/docker-update-readme.yml @@ -9,9 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Docker Hub Description uses: peter-evans/dockerhub-description@v4 - env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - DOCKERHUB_REPOSITORY: freqtradeorg/freqtrade + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + repository: freqtradeorg/freqtrade diff --git a/.github/workflows/pre-commit-update.yml b/.github/workflows/pre-commit-update.yml index 13188af14..69f5dbb4e 100644 --- a/.github/workflows/pre-commit-update.yml +++ b/.github/workflows/pre-commit-update.yml @@ -1,7 +1,6 @@ name: Pre-commit auto-update on: - # every day at midnight schedule: - cron: "0 3 * * 2" # on demand @@ -14,32 +13,32 @@ jobs: auto-update: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.11" + - uses: actions/setup-python@v5 + with: + python-version: "3.11" - - name: Install pre-commit - run: pip install pre-commit + - name: Install pre-commit + run: pip install pre-commit - - name: Run auto-update - run: pre-commit autoupdate + - name: Run auto-update + run: pre-commit autoupdate - - name: Run pre-commit - run: pre-commit run --all-files + - name: Run pre-commit + run: pre-commit run --all-files - - uses: peter-evans/create-pull-request@v6 - with: - token: ${{ secrets.REPO_SCOPED_TOKEN }} - add-paths: .pre-commit-config.yaml - labels: | - Tech maintenance - Dependencies - branch: update/pre-commit-hooks - title: Update pre-commit hooks - commit-message: "chore: update pre-commit hooks" - committer: Freqtrade Bot - body: Update versions of pre-commit hooks to latest version. - delete-branch: true + - uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.REPO_SCOPED_TOKEN }} + add-paths: .pre-commit-config.yaml + labels: | + Tech maintenance + Dependencies + branch: update/pre-commit-hooks + title: Update pre-commit hooks + commit-message: "chore: update pre-commit hooks" + committer: Freqtrade Bot + body: Update versions of pre-commit hooks to latest version. + delete-branch: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 842c87976..0ace86868 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,17 +9,17 @@ repos: # stages: [push] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.8.0" + rev: "v1.9.0" hooks: - id: mypy exclude: build_helpers additional_dependencies: - types-cachetools==5.3.0.7 - types-filelock==3.2.7 - - types-requests==2.31.0.20240218 + - types-requests==2.31.0.20240311 - types-tabulate==0.9.0.20240106 - - types-python-dateutil==2.8.19.20240106 - - SQLAlchemy==2.0.27 + - types-python-dateutil==2.9.0.20240316 + - SQLAlchemy==2.0.29 # stages: [push] - repo: https://github.com/pycqa/isort @@ -31,7 +31,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.2.2' + rev: 'v0.3.4' hooks: - id: ruff diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0d8f337b..f9ab29592 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ pytest tests/test_.py::test_ #### Run Ruff ```bash -ruff . +ruff check . ``` We receive a lot of code that fails the `ruff` checks. diff --git a/Dockerfile b/Dockerfile index a1205f219..6e8992b8b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11.8-slim-bookworm as base +FROM python:3.12.2-slim-bookworm as base # Setup env ENV LANG C.UTF-8 diff --git a/build_helpers/binance_update_lev_tiers.py b/build_helpers/binance_update_lev_tiers.py new file mode 100644 index 000000000..c0f3a7c1c --- /dev/null +++ b/build_helpers/binance_update_lev_tiers.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import json +import os +from pathlib import Path + +import ccxt + + +key = os.environ.get('FREQTRADE__EXCHANGE__KEY') +secret = os.environ.get('FREQTRADE__EXCHANGE__SECRET') + +proxy = os.environ.get('CI_WEB_PROXY') + +exchange = ccxt.binance({ + 'apiKey': key, + 'secret': secret, + 'httpsProxy': proxy, + 'options': {'defaultType': 'swap'} + }) +_ = exchange.load_markets() + +lev_tiers = exchange.fetch_leverage_tiers() + +# Assumes this is running in the root of the repository. +file = Path('freqtrade/exchange/binance_leverage_tiers.json') +json.dump(dict(sorted(lev_tiers.items())), file.open('w'), indent=2) diff --git a/build_helpers/freqtrade_client_version_align.py b/build_helpers/freqtrade_client_version_align.py new file mode 100755 index 000000000..3e2c32e20 --- /dev/null +++ b/build_helpers/freqtrade_client_version_align.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +from freqtrade_client import __version__ as client_version + +from freqtrade import __version__ as ft_version + + +def main(): + if ft_version != client_version: + print(f"Versions do not match: \n" + f"ft: {ft_version} \n" + f"client: {client_version}") + exit(1) + print(f"Versions match: ft: {ft_version}, client: {client_version}") + exit(0) + + +if __name__ == '__main__': + main() diff --git a/build_helpers/pyarrow-15.0.0-cp311-cp311-linux_armv7l.whl b/build_helpers/pyarrow-15.0.2-cp311-cp311-linux_armv7l.whl similarity index 66% rename from build_helpers/pyarrow-15.0.0-cp311-cp311-linux_armv7l.whl rename to build_helpers/pyarrow-15.0.2-cp311-cp311-linux_armv7l.whl index 11cfc1895..77545d082 100644 Binary files a/build_helpers/pyarrow-15.0.0-cp311-cp311-linux_armv7l.whl and b/build_helpers/pyarrow-15.0.2-cp311-cp311-linux_armv7l.whl differ diff --git a/build_helpers/pyarrow-15.0.0-cp39-cp39-linux_armv7l.whl b/build_helpers/pyarrow-15.0.2-cp39-cp39-linux_armv7l.whl similarity index 66% rename from build_helpers/pyarrow-15.0.0-cp39-cp39-linux_armv7l.whl rename to build_helpers/pyarrow-15.0.2-cp39-cp39-linux_armv7l.whl index fe2a1e85a..638750fc2 100644 Binary files a/build_helpers/pyarrow-15.0.0-cp39-cp39-linux_armv7l.whl and b/build_helpers/pyarrow-15.0.2-cp39-cp39-linux_armv7l.whl differ diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 3926fb5b1..e91842d64 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -109,12 +109,12 @@ automatically accessible by including them on the indicator-list, and these incl - **open_date :** trade open datetime - **close_date :** trade close datetime - **min_rate :** minimum price seen throughout the position -- **max_rate :** maxiumum price seen throughout the position +- **max_rate :** maximum price seen throughout the position - **open :** signal candle open price - **close :** signal candle close price - **high :** signal candle high price - **low :** signal candle low price -- **volume :** signal candle volumne +- **volume :** signal candle volume - **profit_ratio :** trade profit ratio - **profit_abs :** absolute profit return of the trade diff --git a/docs/assets/show-config-output.png b/docs/assets/show-config-output.png new file mode 100644 index 000000000..010c5b8a4 Binary files /dev/null and b/docs/assets/show-config-output.png differ diff --git a/docs/backtesting.md b/docs/backtesting.md index ece3ce7fa..11510dc24 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -252,34 +252,34 @@ The most important in the backtesting is to understand the result. A backtesting result will look like that: ``` -========================================================= BACKTESTING REPORT ========================================================= -| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | -|:---------|--------:|---------------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| -| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | -| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | -| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | -| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | -| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | -| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | -| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | -| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | -| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | -| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | -| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | -| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | -| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | -| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | -| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | -| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | -| 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 | -====================================================== 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 | +================================================ BACKTESTING REPORT ================================================= +| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | +|:---------|--------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| +| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | +| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | +| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | +| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | +| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | +| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | +| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | +| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | +| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | +| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | +| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | +| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | +| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | +| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | +| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | +| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | +| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | +| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | +| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | +============================================= LEFT OPEN TRADES REPORT ============================================= +| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | +|:---------|---------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| +| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | +| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | +| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | ==================== EXIT REASON STATS ==================== | Exit Reason | Exits | Wins | Draws | Losses | |:-------------------|--------:|------:|-------:|--------:| @@ -358,7 +358,7 @@ here: The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. -The column `Avg Profit %` shows the average profit for all trades made while the column `Cum Profit %` sums up all the profits/losses. +The column `Avg Profit %` shows the average profit for all trades made. The column `Tot Profit %` shows instead the total profit % in relation to the starting balance. In the above results, we have a starting balance of 0.01 BTC and an absolute profit of 0.00762792 BTC - so the `Tot Profit %` will be `(0.00762792 / 0.01) * 100 ~= 76.2%`. @@ -464,7 +464,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Profit factor`: profit / loss. - `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount. - `Total trade volume`: Volume generated on the exchange to reach the above profit. -- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. +- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Tot Profit %`. - `Best Trade` / `Worst Trade`: Biggest single winning trade and biggest single losing trade. - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). @@ -629,11 +629,11 @@ There will be an additional table comparing win/losses of the different strategi Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy. ``` -=========================================================== STRATEGY SUMMARY =========================================================================== -| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | -|:------------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| -| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | -| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | +================================================== STRATEGY SUMMARY =================================================================== +| Strategy | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | +|:------------|---------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| +| Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | +| Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | ``` ## Next step diff --git a/docs/bot-basics.md b/docs/bot-basics.md index a19f005db..1c88559c0 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -33,7 +33,6 @@ For spot pairs, naming will be `base/quote` (e.g. `ETH/USDT`). For futures pairs, naming will be `base/quote:settle` (e.g. `ETH/USDT:USDT`). - ## Bot execution logic Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop. @@ -50,10 +49,12 @@ By default, the bot loop runs every few seconds (`internals.process_throttle_sec * Call `populate_indicators()` * Call `populate_entry_trend()` * Call `populate_exit_trend()` -* Check timeouts for open orders. - * Calls `check_entry_timeout()` strategy callback for open entry orders. - * Calls `check_exit_timeout()` strategy callback for open exit orders. - * Calls `adjust_entry_price()` strategy callback for open entry orders. +* Update trades open order state from exchange. + * Call `order_filled()` strategy callback for filled orders. + * Check timeouts for open orders. + * Calls `check_entry_timeout()` strategy callback for open entry orders. + * Calls `check_exit_timeout()` strategy callback for open exit orders. + * Calls `adjust_entry_price()` strategy callback for open entry orders. * Verifies existing positions and eventually places exit orders. * Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`. * Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback. @@ -86,8 +87,10 @@ This loop will be repeated again and again until the bot is stopped. * In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage. * Determine stake size by calling the `custom_stake_amount()` callback. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. + * Call `order_filled()` strategy callback for filled entry orders. * Call `custom_stoploss()` and `custom_exit()` to find custom exit points. * For exits based on exit-signal, custom-exit and partial exits: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). + * Call `order_filled()` strategy callback for filled exit orders. * Generate backtest report output !!! Note diff --git a/docs/configuration.md b/docs/configuration.md index 2fc54668a..684c6743c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -49,6 +49,13 @@ FREQTRADE__EXCHANGE__SECRET= !!! Note Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable. +!!! Tip "Validate combined result" + You can use the [show-config subcommand](utils.md#show-config) to see the final, combined configuration. + +??? Warning "Loading sequence" + Environment variables are loaded after the initial configuration. As such, you cannot provide the path to the configuration through environment variables. Please use `--config path/to/config.json` for that. + This also applies to user_dir to some degree. while the user directory can be set through environment variables - the configuration will **not** be loaded from that location. + ### Multiple configuration files Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. @@ -56,6 +63,9 @@ Multiple configuration files can be specified and used by the bot or the bot can You can specify additional configuration files in `add_config_files`. Files specified in this parameter will be loaded and merged with the initial config file. The files are resolved relative to the initial configuration file. This is similar to using multiple `--config` parameters, but simpler in usage as you don't have to specify all files for all commands. +!!! Tip "Validate combined result" + You can use the [show-config subcommand](utils.md#show-config) to see the final, combined configuration. + !!! Tip "Use multiple configuration files to keep secrets secret" You can use a 2nd configuration file containing your secrets. That way you can share your "primary" configuration file, while still keeping your API keys for yourself. The 2nd file should only specify what you intend to override. @@ -326,6 +336,8 @@ You'd set `available_capital=5000` - granting each bot an initial capital of 500 The bot will then split this starting balance equally into `max_open_trades` buckets. Profitable trades will result in increased stake-sizes for this bot - without affecting the stake-sizes of the other bot. +Adjusting `available_capital` requires reloading the configuration to take effect. Adjusting the `available_capital` adds the difference between the previous `available_capital` and the new `available_capital`. Decreasing the available capital when trades are open doesn't exit the trades. The difference is returned to the wallet when the trades conclude. The outcome of this differs depending on the price movement between the adjustment and exiting the trades. + !!! Warning "Incompatible with `tradable_balance_ratio`" Setting this option will replace any configuration of `tradable_balance_ratio`. @@ -503,13 +515,13 @@ Configuration: Please carefully read the section [Market order pricing](#market-order-pricing) section when using market orders. !!! Note "Stoploss on exchange" - `stoploss_on_exchange_interval` is not mandatory. Do not change its value if you are + `order_types.stoploss_on_exchange_interval` is not mandatory. Do not change its value if you are unsure of what you are doing. For more information about how stoploss works please refer to [the stoploss documentation](stoploss.md). - If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. + If `order_types.stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. -!!! Warning "Warning: stoploss_on_exchange failures" +!!! Warning "Warning: order_types.stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency exit" is initiated. By default, this will exit the trade using a market order. The order-type for the emergency-exit can be changed by setting the `emergency_exit` value in the `order_types` dictionary - however, this is not advised. ### Understand order_time_in_force diff --git a/docs/developer.md b/docs/developer.md index 9c549012d..9e23db656 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -129,6 +129,8 @@ Below is an outline of exception inheritance hierarchy: + FreqtradeException | +---+ OperationalException +| | +| +---+ ConfigurationError | +---+ DependencyException | | @@ -376,7 +378,7 @@ from pathlib import Path exchange = ccxt.binance({ 'apiKey': '', - 'secret': '' + 'secret': '', 'options': {'defaultType': 'swap'} }) _ = exchange.load_markets() diff --git a/docs/exchanges.md b/docs/exchanges.md index c106ea4cf..a8c4a8b4f 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -68,6 +68,8 @@ Binance supports [time_in_force](configuration.md#understand-order_time_in_force For Binance, it is suggested to add `"BNB/"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `BNB` on the account or unless you're willing to disable using `BNB` for fees. Binance accounts may use `BNB` for fees, and if a trade happens to be on `BNB`, further trades may consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. +If not enough `BNB` is available to cover transaction fees, then fees will not be covered by `BNB` and no fee reduction will occur. Freqtrade will never buy BNB to cover for fees. BNB needs to be bought and monitored manually to this end. + ### Binance sites Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index dd0dde5d4..5caa89a42 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -32,6 +32,9 @@ FreqAI is configured through the typical [Freqtrade config file](configuration.m A full example config is available in `config_examples/config_freqai.example.json`. +!!! Note + The `identifier` is commonly overlooked by newcomers, however, this value plays an important role in your configuration. This value is a unique ID that you choose to describe one of your runs. Keeping it the same allows you to maintain crash resilience as well as faster backtesting. As soon as you want to try a new run (new features, new model, etc.), you should change this value (or delete the `user_data/models/unique-id` folder. More details available in the [parameter table](freqai-parameter-table.md#feature-parameters). + ## Building a FreqAI strategy The FreqAI strategy requires including the following lines of code in the standard [Freqtrade strategy](strategy-customization.md): diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 905ea479a..055b7b45d 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -75,7 +75,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model.
**Datatype:** Dictionary. | `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points.
**Datatype:** Integer. | `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function.
**Datatype:** int. -| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website)
**Datatype:** string. +| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentation. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website)
**Datatype:** string. | `policy_type` | One of the available policy types from stable_baselines3
**Datatype:** string. | `max_training_drawdown_pct` | The maximum drawdown that the agent is allowed to experience during training.
**Datatype:** float.
Default: 0.8 | `cpu_count` | Number of threads/cpus to dedicate to the Reinforcement Learning training process (depending on if `ReinforcementLearning_multiproc` is selected or not). Recommended to leave this untouched, by default, this value is set to the total number of physical cores minus 1.
**Datatype:** int. diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index c5cda3bc3..3b75e6b71 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -142,7 +142,7 @@ Parameter details can be found [here](freqai-parameter-table.md), but in general 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. + 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 occurring at a single point in time. ```python from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index d1dd2cda7..0f01717ab 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -81,12 +81,14 @@ Filtering instances (not the first position in the list) will not apply any cach "number_assets": 20, "sort_key": "quoteVolume", "min_value": 0, + "max_value": 8000000, "refresh_period": 1800 } ], ``` You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange. +In addition to that, you can also define a maximum volume with `max_value` - which will filter out pairs with a volume higher than the specified value in the specified timerange. ##### VolumePairList Advanced mode @@ -369,6 +371,11 @@ As this Filter uses past performance of the bot, it'll have some startup-period Filters low-value coins which would not allow setting stoplosses. +Namely, pairs are blacklisted if a variance of one percent or more in the stop price would be caused by precision rounding on the exchange, i.e. `rounded(stop_price) <= rounded(stop_price * 0.99)`. The idea is to avoid coins with a value VERY close to their lower trading boundary, not allowing setting of proper stoploss. + +!!! Tip "PerformanceFilter is pointless for futures trading" + The above does not apply to shorts. And for longs, in theory the trade will be liquidated first. + !!! Warning "Backtesting" `PrecisionFilter` does not support backtesting mode using multiple strategies. @@ -450,6 +457,8 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from ] ``` +Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting for this pairlist. + !!! Tip This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. Additionally, it can also be used to automatically remove pairs with extreme high/low variance over a given amount of time. @@ -460,7 +469,7 @@ Volatility is the degree of historical variation of a pairs over time, it is mea This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. -This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. +This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. In the below example: If the volatility over the last 10 days is not in the range of 0.05-0.50, remove the pair from the whitelist. The filter is applied every 24h. @@ -477,6 +486,8 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove ] ``` +Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting mode for this pairlist. + ### Full example of Pairlist Handlers The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#pricefilter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. diff --git a/docs/lookahead-analysis.md b/docs/lookahead-analysis.md index e998b1b77..90ba7041a 100644 --- a/docs/lookahead-analysis.md +++ b/docs/lookahead-analysis.md @@ -23,6 +23,7 @@ It also supports the lookahead-analysis of freqai strategies. - `--max-open-trades` is forced to be at least equal to the number of pairs. - `--dry-run-wallet` is forced to be basically infinite (1 billion). - `--stake-amount` is forced to be a static 10000 (10k). +- `--enable-protections` is forced to be off. Those are set to avoid users accidentally generating false positives. @@ -40,7 +41,6 @@ usage: freqtrade lookahead-analysis [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [-p PAIRS [PAIRS ...]] - [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [--timeframe-detail TIMEFRAME_DETAIL] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index cbb81b6b2..7fafe1598 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ -markdown==3.5.2 +markdown==3.6 mkdocs==1.5.3 -mkdocs-material==9.5.11 +mkdocs-material==9.5.15 mdx_truly_sane_lists==1.3 -pymdown-extensions==10.7 +pymdown-extensions==10.7.1 jinja2==3.1.3 diff --git a/docs/rest-api.md b/docs/rest-api.md index 229fa5f94..69a0f31f5 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -95,11 +95,13 @@ Make sure that the following 2 lines are available in your docker-compose file: ### Consuming the API -You can consume the API by using the script `scripts/rest_client.py`. -The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system. +You can consume the API by using `freqtrade-client` (also available as `scripts/rest_client.py`). +This command can be installed independent of the bot by using `pip install freqtrade-client`. + +This module is designed to be lightweight, and only depends on the `requests` and `python-rapidjson` modules, skipping all heavy dependencies freqtrade otherwise needs. ``` bash -python3 scripts/rest_client.py [optional parameters] +freqtrade-client [optional parameters] ``` By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. @@ -120,9 +122,27 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use ``` ``` bash -python3 scripts/rest_client.py --config rest_config.json [optional parameters] +freqtrade-client --config rest_config.json [optional parameters] ``` +??? Note "Programmatic use" + The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API. + to do so, please use the following: + + ``` python + from freqtrade_client import FtRestClient + + + client = FtRestClient(server_url, username, password) + + # Get the status of the bot + ping = client.ping() + print(ping) + # ... + ``` + + For a full list of available commands, please refer to the list below. + ### Available endpoints | Command | Description | @@ -176,7 +196,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par Possible commands can be listed from the rest-client script using the `help` command. ``` bash -python3 scripts/rest_client.py help +freqtrade-client help ``` ``` output diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2749d1281..debd5bc1b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -11,34 +11,129 @@ The call sequence of the methods described here is covered under [bot execution !!! Tip Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` -## Storing information +## Storing information (Persistent) -Storing information can be accomplished by creating a new dictionary within the strategy class. +Freqtrade allows storing/retrieving user custom information associated with a specific trade in the database. -The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. +Using a trade object, information can be stored using `trade.set_custom_data(key='my_key', value=my_value)` and retrieved using `trade.get_custom_data(key='my_key')`. Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object. + +For the data to be able to be stored within the database, freqtrade must serialized the data. This is done by converting the data to a JSON formatted string. +Freqtrade will attempt to reverse this action on retrieval, so from a strategy perspective, this should not be relevant. ```python +from freqtrade.persistence import Trade +from datetime import timedelta + class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # Check if the entry already exists - if not metadata["pair"] in self.custom_info: - # Create empty entry for this pair - self.custom_info[metadata["pair"]] = {} + def bot_loop_start(self, **kwargs) -> None: + for trade in Trade.get_open_order_trades(): + fills = trade.select_filled_orders(trade.entry_side) + if trade.pair == 'ETH/USDT': + trade_entry_type = trade.get_custom_data(key='entry_type') + if trade_entry_type is None: + trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip' + elif fills > 1: + trade_entry_type = 'buy_up' + trade.set_custom_data(key='entry_type', value=trade_entry_type) + return super().bot_loop_start(**kwargs) - if "crosstime" in self.custom_info[metadata["pair"]]: - self.custom_info[metadata["pair"]]["crosstime"] += 1 - else: - self.custom_info[metadata["pair"]]["crosstime"] = 1 + def adjust_entry_price(self, trade: Trade, order: Optional[Order], pair: str, + current_time: datetime, proposed_rate: float, current_order_rate: float, + entry_tag: Optional[str], side: str, **kwargs) -> float: + # 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 + and order.filled == 0.0 + ): + dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) + current_candle = dataframe.iloc[-1].squeeze() + # store information about entry adjustment + existing_count = trade.get_custom_data('num_entry_adjustments', default=0) + if not existing_count: + existing_count = 1 + else: + existing_count += 1 + trade.set_custom_data(key='num_entry_adjustments', value=existing_count) + + # adjust order price + return current_candle['sma_200'] + + # default: maintain existing order + return current_order_rate + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): + + entry_adjustment_count = trade.get_custom_data(key='num_entry_adjustments') + trade_entry_type = trade.get_custom_data(key='entry_type') + if entry_adjustment_count is None: + if current_profit > 0.01 and (current_time - timedelta(minutes=100) > trade.open_date_utc): + return True, 'exit_1' + else + if entry_adjustment_count > 0 and if current_profit > 0.05: + return True, 'exit_2' + if trade_entry_type == 'breakout' and current_profit > 0.1: + return True, 'exit_3 + + return False, None ``` -!!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. +The above is a simple example - there are simpler ways to retrieve trade data like entry-adjustments. !!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + It is recommended that simple data types are used `[bool, int, float, str]` to ensure no issues when serializing the data that needs to be stored. + Storing big junks of data may lead to unintended side-effects, like a database becoming big (and as a consequence, also slow). + +!!! Warning "Non-serializable data" + If supplied data cannot be serialized a warning is logged and the entry for the specified `key` will contain `None` as data. + +??? Note "All attributes" + custom-data has the following accessors through the Trade object (assumed as `trade` below): + + * `trade.get_custom_data(key='something', default=0)` - Returns the actual value given in the type provided. + * `trade.get_custom_data_entry(key='something')` - Returns the entry - including metadata. The value is accessible via `.value` property. + * `trade.set_custom_data(key='something', value={'some': 'value'})` - set or update the corresponding key for this trade. Value must be serializable - and we recommend to keep the stored data relatively small. + + "value" can be any type (both in setting and receiving) - but must be json serializable. + +## Storing information (Non-Persistent) + +!!! Warning "Deprecated" + This method of storing information is deprecated and we do advise against using non-persistent storage. + Please use [Persistent Storage](#storing-information-persistent) instead. + + It's content has therefore been collapsed. + +??? Abstract "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 `custom_` to avoid naming collisions with predefined strategy variables. + + ```python + class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Check if the entry already exists + if not metadata["pair"] in self.custom_info: + # Create empty entry for this pair + self.custom_info[metadata["pair"]] = {} + + if "crosstime" in self.custom_info[metadata["pair"]]: + self.custom_info[metadata["pair"]]["crosstime"] += 1 + else: + self.custom_info[metadata["pair"]]["crosstime"] = 1 + ``` + + !!! Warning + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + + !!! Note + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. ## Dataframe access diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 2f04e906e..74dc40651 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -19,6 +19,7 @@ Currently available callbacks: * [`adjust_trade_position()`](#adjust-trade-position) * [`adjust_entry_price()`](#adjust-entry-price) * [`leverage()`](#leverage-callback) +* [`order_filled()`](#order-filled-callback) !!! Tip "Callback calling sequence" You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic) @@ -783,7 +784,7 @@ Additional entries are ignored once you have reached the maximum amount of extra ### Decrease position The strategy is expected to return a negative stake_amount (in stake currency) for a partial exit. -Returning the full owned stake at that point (based on the current price) (`-(trade.amount / trade.leverage) * current_exit_rate`) results in a full exit. +Returning the full owned stake at that point (`-trade.stake_amount`) results in a full exit. Returning a value more than the above (so remaining stake_amount would become negative) will result in the bot ignoring the signal. !!! Note "About stake size" @@ -1022,3 +1023,33 @@ class AwesomeStrategy(IStrategy): All profit calculations include leverage. Stoploss / ROI also include leverage in their calculation. Defining a stoploss of 10% at 10x leverage would trigger the stoploss with a 1% move to the downside. + +## Order filled Callback + +The `order_filled()` callback may be used to perform specific actions based on the current trade state after an order is filled. +It will be called independent of the order type (entry, exit, stoploss or position adjustment). + +Assuming that your strategy needs to store the high value of the candle at trade entry, this is possible with this callback as the following example show. + +``` python +class AwesomeStrategy(IStrategy): + def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None: + """ + Called right after an order fills. + Will be called for all order types (entry, exit, stoploss, position adjustment). + :param pair: Pair for trade + :param trade: trade object. + :param order: Order object. + :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. + """ + # Obtain pair dataframe (just to show how to access it) + dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): + trade.set_custom_data(key='entry_candle_high', value=last_candle['high']) + + return None + +``` diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 846c53238..22828b899 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -19,7 +19,7 @@ from pathlib import Path project_root = "somedir/freqtrade" i=0 try: - os.chdirdir(project_root) + os.chdir(project_root) assert Path('LICENSE').is_file() except: while i<4 and (not Path('LICENSE').is_file()): diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index e4dc02c76..f878fc2b3 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -59,7 +59,7 @@ For the Freqtrade configuration, you can then use the the full value (including "chat_id": "-1001332619709" ``` !!! Warning "Using telegram groups" - When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasent surprises. + When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasant surprises. ## Control telegram noise @@ -181,6 +181,7 @@ official commands. You can ask at any moment for help with `/help`. | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). | `/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. +| `/list_custom_data [key]` | List custom_data for Trade ID & Key combination. If no Key is supplied it will list all key-value pairs found for that Trade ID. | **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`). diff --git a/docs/updating.md b/docs/updating.md index 1e5dc8ffe..5841d205a 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -6,7 +6,7 @@ To update your freqtrade installation, please use one of the below methods, corr Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release. For the develop branch, please follow PR's to avoid being surprised by changes. -## docker +## Docker !!! Note "Legacy installations using the `master` image" We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable` diff --git a/docs/utils.md b/docs/utils.md index 202526afe..a11e7b698 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -66,6 +66,53 @@ $ freqtrade new-config --config user_data/config_binance.json ? Do you want to enable Telegram? No ``` +## Show config + +Show configuration file (with sensitive values redacted by default). +Especially useful with [split configuration files](configuration.md#multiple-configuration-files) or [environment variables](configuration.md#environment-variables), where this command will show the merged configuration. + +![Show config output](assets/show-config-output.png) + +``` +usage: freqtrade show-config [-h] [--userdir PATH] [-c PATH] + [--show-sensitive] + +options: + -h, --help show this help message and exit + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + -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. + --show-sensitive Show secrets in the output. +``` + +``` output +Your combined configuration is: +{ + "exit_pricing": { + "price_side": "other", + "use_order_book": true, + "order_book_top": 1 + }, + "stake_currency": "USDT", + "exchange": { + "name": "binance", + "key": "REDACTED", + "secret": "REDACTED", + "ccxt_config": {}, + "ccxt_async_config": {}, + } + // ... +} +``` + +!!! Warning "Sharing information provided by this command" + We try to remove all known sensitive information from the default output (without `--show-sensitive`). + Yet, please do double-check for sensitive values in your output to make sure you're not accidentally exposing some private info. + ## Create new strategy Creates a new strategy from a template similar to SampleStrategy. @@ -219,207 +266,49 @@ optional arguments: -a, --all Print all exchanges known to the ccxt library. ``` -* Example: see exchanges available for the bot: +Example: see exchanges available for the bot: + ``` $ freqtrade list-exchanges Exchanges available for Freqtrade: -Exchange name Valid reason ---------------- ------- -------------------------------------------- -aax True -ascendex True missing opt: fetchMyTrades -bequant True -bibox True -bigone True -binance True -binanceus True -bitbank True missing opt: fetchTickers -bitcoincom True -bitfinex True -bitforex True missing opt: fetchMyTrades, fetchTickers -bitget True -bithumb True missing opt: fetchMyTrades -bitkk True missing opt: fetchMyTrades -bitmart True -bitmax True missing opt: fetchMyTrades -bitpanda True -bitvavo True -bitz True missing opt: fetchMyTrades -btcalpha True missing opt: fetchTicker, fetchTickers -btcmarkets True missing opt: fetchTickers -buda True missing opt: fetchMyTrades, fetchTickers -bw True missing opt: fetchMyTrades, fetchL2OrderBook -bybit True -bytetrade True -cdax True -cex True missing opt: fetchMyTrades -coinbaseprime True missing opt: fetchTickers -coinbasepro True missing opt: fetchTickers -coinex True -crex24 True -deribit True -digifinex True -equos True missing opt: fetchTicker, fetchTickers -eterbase True -fcoin True missing opt: fetchMyTrades, fetchTickers -fcoinjp True missing opt: fetchMyTrades, fetchTickers -gateio True -gemini True -gopax True -hbtc True -hitbtc True -huobijp True -huobipro True -idex True -kraken True -kucoin True -lbank True missing opt: fetchMyTrades -mercado True missing opt: fetchTickers -ndax True missing opt: fetchTickers -novadax True -okcoin True -okex True -probit True -qtrade True -stex True -timex True -upbit True missing opt: fetchMyTrades -vcc True -zb True missing opt: fetchMyTrades - +Exchange name Supported Markets Reason +------------------ ----------- ---------------------- ------------------------------------------------------------------------ +binance Official spot, isolated futures +bitmart Official spot +bybit spot, isolated futures +gate Official spot, isolated futures +htx Official spot +huobi spot +kraken Official spot +okx Official spot, isolated futures ``` +!!! info "" + Output reduced for clarity - supported and available exchanges may change over time. + !!! Note "missing opt exchanges" Values with "missing opt:" might need special configuration (e.g. using orderbook if `fetchTickers` is missing) - but should in theory work (although we cannot guarantee they will). -* Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade): +Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade) + ``` $ freqtrade list-exchanges -a All exchanges supported by the ccxt library: -Exchange name Valid reason ------------------- ------- --------------------------------------------------------------------------------------- -aax True -aofex False missing: fetchOrder -ascendex True missing opt: fetchMyTrades -bequant True -bibox True -bigone True -binance True -binanceus True -bit2c False missing: fetchOrder, fetchOHLCV -bitbank True missing opt: fetchTickers -bitbay False missing: fetchOrder -bitcoincom True -bitfinex True -bitfinex2 False missing: fetchOrder -bitflyer False missing: fetchOrder, fetchOHLCV -bitforex True missing opt: fetchMyTrades, fetchTickers -bitget True -bithumb True missing opt: fetchMyTrades -bitkk True missing opt: fetchMyTrades -bitmart True -bitmax True missing opt: fetchMyTrades -bitmex False Various reasons. -bitpanda True -bitso False missing: fetchOHLCV -bitstamp True missing opt: fetchTickers -bitstamp1 False missing: fetchOrder, fetchOHLCV -bitvavo True -bitz True missing opt: fetchMyTrades -bl3p False missing: fetchOrder, fetchOHLCV -bleutrade False missing: fetchOrder -braziliex False missing: fetchOHLCV -btcalpha True missing opt: fetchTicker, fetchTickers -btcbox False missing: fetchOHLCV -btcmarkets True missing opt: fetchTickers -btctradeua False missing: fetchOrder, fetchOHLCV -btcturk False missing: fetchOrder -buda True missing opt: fetchMyTrades, fetchTickers -bw True missing opt: fetchMyTrades, fetchL2OrderBook -bybit True -bytetrade True -cdax True -cex True missing opt: fetchMyTrades -chilebit False missing: fetchOrder, fetchOHLCV -coinbase False missing: fetchOrder, cancelOrder, createOrder, fetchOHLCV -coinbaseprime True missing opt: fetchTickers -coinbasepro True missing opt: fetchTickers -coincheck False missing: fetchOrder, fetchOHLCV -coinegg False missing: fetchOHLCV -coinex True -coinfalcon False missing: fetchOHLCV -coinfloor False missing: fetchOrder, fetchOHLCV -coingi False missing: fetchOrder, fetchOHLCV -coinmarketcap False missing: fetchOrder, cancelOrder, createOrder, fetchBalance, fetchOHLCV -coinmate False missing: fetchOHLCV -coinone False missing: fetchOHLCV -coinspot False missing: fetchOrder, cancelOrder, fetchOHLCV -crex24 True -currencycom False missing: fetchOrder -delta False missing: fetchOrder -deribit True -digifinex True -equos True missing opt: fetchTicker, fetchTickers -eterbase True -exmo False missing: fetchOrder -exx False missing: fetchOHLCV -fcoin True missing opt: fetchMyTrades, fetchTickers -fcoinjp True missing opt: fetchMyTrades, fetchTickers -flowbtc False missing: fetchOrder, fetchOHLCV -foxbit False missing: fetchOrder, fetchOHLCV -gateio True -gemini True -gopax True -hbtc True -hitbtc True -hollaex False missing: fetchOrder -huobijp True -huobipro True -idex True -independentreserve False missing: fetchOHLCV -indodax False missing: fetchOHLCV -itbit False missing: fetchOHLCV -kraken True -kucoin True -kuna False missing: fetchOHLCV -lakebtc False missing: fetchOrder, fetchOHLCV -latoken False missing: fetchOrder, fetchOHLCV -lbank True missing opt: fetchMyTrades -liquid False missing: fetchOHLCV -luno False missing: fetchOHLCV -lykke False missing: fetchOHLCV -mercado True missing opt: fetchTickers -mixcoins False missing: fetchOrder, fetchOHLCV -ndax True missing opt: fetchTickers -novadax True -oceanex False missing: fetchOHLCV -okcoin True -okex True -paymium False missing: fetchOrder, fetchOHLCV -phemex False Does not provide history. -poloniex False missing: fetchOrder -probit True -qtrade True -rightbtc False missing: fetchOrder -ripio False missing: fetchOHLCV -southxchange False missing: fetchOrder, fetchOHLCV -stex True -surbitcoin False missing: fetchOrder, fetchOHLCV -therock False missing: fetchOHLCV -tidebit False missing: fetchOrder -tidex False missing: fetchOHLCV -timex True -upbit True missing opt: fetchMyTrades -vbtc False missing: fetchOrder, fetchOHLCV -vcc True -wavesexchange False missing: fetchOrder -whitebit False missing: fetchOrder, cancelOrder, createOrder, fetchBalance -xbtce False missing: fetchOrder, fetchOHLCV -xena False missing: fetchOrder -yobit False missing: fetchOHLCV -zaif False missing: fetchOrder, fetchOHLCV -zb True missing opt: fetchMyTrades +Exchange name Valid Supported Markets Reason +------------------ ------- ----------- ---------------------- --------------------------------------------------------------------------------- +binance True Official spot, isolated futures +bitflyer False spot missing: fetchOrder. missing opt: fetchTickers. +bitmart True Official spot +bybit True spot, isolated futures +gate True Official spot, isolated futures +htx True Official spot +kraken True Official spot +okx True Official spot, isolated futures ``` +!!! info "" + Reduced output - supported and available exchanges may change over time. + ## List Timeframes Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange. diff --git a/docs/webhook-config.md b/docs/webhook-config.md index b4044655c..9125ff361 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -65,7 +65,7 @@ You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw The result would be a POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel. -When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be ouput in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example: +When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be output in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example: ```json "webhook": { diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 32033cb45..347f14a07 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2024.2' +__version__ = '2024.3' if 'dev' in __version__: from pathlib import Path diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 98e7cb084..e0fa0fd51 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -8,7 +8,7 @@ Note: Be careful with file-scoped imports in these subfiles. """ from freqtrade.commands.analyze_commands import start_analysis_entries_exits from freqtrade.commands.arguments import Arguments -from freqtrade.commands.build_config_commands import start_new_config +from freqtrade.commands.build_config_commands import start_new_config, start_show_config from freqtrade.commands.data_commands import (start_convert_data, start_convert_trades, start_download_data, start_list_data) from freqtrade.commands.db_commands import start_convert_db diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py index e928ccad7..d271a82e3 100644 --- a/freqtrade/commands/analyze_commands.py +++ b/freqtrade/commands/analyze_commands.py @@ -4,7 +4,7 @@ from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException logger = logging.getLogger(__name__) @@ -34,9 +34,9 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s btfile = Path(config['exportfilename']) signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl" else: - raise OperationalException(f"{config['exportfilename']} does not exist.") + raise ConfigurationError(f"{config['exportfilename']} does not exist.") else: - raise OperationalException('exportfilename not in config.') + raise ConfigurationError('exportfilename not in config.') if (not Path(signals_file).exists()): raise OperationalException( diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index f72164675..55c3aa586 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -62,6 +62,7 @@ ARGS_TEST_PAIRLIST = ["user_data_dir", "verbosity", "config", "quote_currencies" ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] ARGS_BUILD_CONFIG = ["config"] +ARGS_SHOW_CONFIG = ["user_data_dir", "config", "show_sensitive"] ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] @@ -69,7 +70,8 @@ ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"] -ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"] +ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades", + "trading_mode"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"] @@ -208,9 +210,9 @@ class Arguments: start_list_strategies, start_list_timeframes, start_lookahead_analysis, start_new_config, start_new_strategy, start_plot_dataframe, start_plot_profit, - start_recursive_analysis, start_show_trades, - start_strategy_update, start_test_pairlist, start_trading, - start_webserver) + start_recursive_analysis, start_show_config, + 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 @@ -243,6 +245,14 @@ class Arguments: build_config_cmd.set_defaults(func=start_new_config) self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd) + # add show-config subcommand + show_config_cmd = subparsers.add_parser( + 'show-config', + help="Show resolved config", + ) + show_config_cmd.set_defaults(func=start_show_config) + self._build_args(optionlist=ARGS_SHOW_CONFIG, parser=show_config_cmd) + # add new-strategy subcommand build_strategy_cmd = subparsers.add_parser( 'new-strategy', diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 5b3447d70..63862c3bf 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -5,9 +5,12 @@ from typing import Any, Dict, List from questionary import Separator, prompt +from freqtrade.configuration import sanitize_config +from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.detect_environment import running_in_docker from freqtrade.configuration.directory_operations import chown_user_directory from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges from freqtrade.util import render_template @@ -264,3 +267,19 @@ def start_new_config(args: Dict[str, Any]) -> None: "Please delete it or use a different configuration file name.") selections = ask_user_config() deploy_new_config(config_path, selections) + + +def start_show_config(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE, set_dry=False) + + # TODO: Sanitize from sensitive info before printing + + print("Your combined configuration is:") + config_sanitized = sanitize_config( + config['original_config'], + show_sensitive=args.get('show_sensitive', False) + ) + + from rich import print_json + print_json(data=config_sanitized) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 4d2dcb1fe..287ab2594 100755 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -716,4 +716,10 @@ AVAILABLE_CLI_OPTIONS = { help='Specify startup candles to be checked (`199`, `499`, `999`, `1999`).', nargs='+', ), + "show_sensitive": Arg( + '--show-sensitive', + help='Show secrets in the output.', + action='store_true', + default=False, + ), } diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 33069885a..6762a83da 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -8,9 +8,10 @@ from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Confi from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, convert_trades_to_ohlcv) from freqtrade.data.history import download_data_main -from freqtrade.enums import RunMode, TradingMode -from freqtrade.exceptions import OperationalException +from freqtrade.enums import CandleType, RunMode, TradingMode +from freqtrade.exceptions import ConfigurationError from freqtrade.exchange import timeframe_to_minutes +from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist from freqtrade.resolvers import ExchangeResolver from freqtrade.util.migrations import migrate_data @@ -20,11 +21,11 @@ logger = logging.getLogger(__name__) def _check_data_config_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.") + raise ConfigurationError("--days and --timerange are mutually exclusive. " + "You can only specify one or the other.") if 'pairs' not in config: - raise OperationalException( + raise ConfigurationError( "Downloading data requires a list of pairs. " "Please check the documentation on how to configure this.") @@ -62,13 +63,21 @@ def start_convert_trades(args: Dict[str, Any]) -> None: for timeframe in config['timeframes']: exchange.validate_timeframes(timeframe) + available_pairs = [ + p for p in exchange.get_markets( + tradable_only=True, active_only=not config.get('include_inactive') + ).keys() + ] + + expanded_pairs = dynamic_expand_pairlist(config, available_pairs) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( - pairs=config.get('pairs', []), timeframes=config['timeframes'], + pairs=expanded_pairs, timeframes=config['timeframes'], datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format_ohlcv=config['dataformat_ohlcv'], data_format_trades=config['dataformat_trades'], + candle_type=config.get('candle_type_def', CandleType.SPOT) ) @@ -98,7 +107,7 @@ def start_list_data(args: Dict[str, Any]) -> None: from tabulate import tabulate - from freqtrade.data.history.idatahandler import get_datahandler + from freqtrade.data.history import get_datahandler dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv']) paircombs = dhc.ohlcv_get_available_data( diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 75da2552e..c87f55e43 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -9,7 +9,7 @@ from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.util import render_template, render_template_with_fallback @@ -89,7 +89,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None: deploy_new_strategy(args['strategy'], new_path, args['template']) else: - raise OperationalException("`new-strategy` requires --strategy to be set.") + raise ConfigurationError("`new-strategy` requires --strategy to be set.") def clean_ui_subdir(directory: Path): diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 84f237f77..550c29f69 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -10,7 +10,7 @@ from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.exchange import list_available_exchanges, market_is_active from freqtrade.misc import parse_db_uri_for_logging, plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -246,7 +246,7 @@ def start_show_trades(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if 'db_url' not in config: - raise OperationalException("--db-url is required for this command.") + raise ConfigurationError("--db-url is required for this command.") logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') init_db(config['db_url']) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 837a5e4f3..f010a3cee 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -4,7 +4,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.util import fmt_coin @@ -31,7 +31,7 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ and config['stake_amount'] > wallet_size): wallet = fmt_coin(wallet_size, config['stake_currency']) stake = fmt_coin(config['stake_amount'], config['stake_currency']) - raise OperationalException( + raise ConfigurationError( f"Starting balance ({wallet}) is smaller than stake_amount {stake}. " f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`." ) diff --git a/freqtrade/commands/plot_commands.py b/freqtrade/commands/plot_commands.py index 73a429a28..95ad3cdce 100644 --- a/freqtrade/commands/plot_commands.py +++ b/freqtrade/commands/plot_commands.py @@ -2,12 +2,12 @@ from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError def validate_plot_args(args: Dict[str, Any]) -> None: if not args.get('datadir') and not args.get('config'): - raise OperationalException( + raise ConfigurationError( "You need to specify either `--datadir` or `--config` " "for plot-profit and plot-dataframe.") diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index 0707cc803..c7f7e524b 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -23,11 +23,6 @@ def start_trading(args: Dict[str, Any]) -> int: 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): - logger.info('SIGINT received, aborting ...') finally: if worker: logger.info("worker found ... calling exit") diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 05921606c..aa06a70c9 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.configuration.config_secrets import sanitize_config from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.configuration import Configuration diff --git a/freqtrade/configuration/config_secrets.py b/freqtrade/configuration/config_secrets.py new file mode 100644 index 000000000..47ee741bf --- /dev/null +++ b/freqtrade/configuration/config_secrets.py @@ -0,0 +1,36 @@ +from copy import deepcopy + +from freqtrade.constants import Config + + +def sanitize_config(config: Config, *, show_sensitive: bool = False) -> Config: + """ + Remove sensitive information from the config. + :param config: Configuration + :param show_sensitive: Show sensitive information + :return: Configuration + """ + if show_sensitive: + return config + keys_to_remove = [ + "exchange.key", + "exchange.secret", + "exchange.password", + "exchange.uid", + "telegram.token", + "telegram.chat_id", + "discord.webhook_url", + "api_server.password", + ] + config = deepcopy(config) + for key in keys_to_remove: + if '.' in key: + nested_keys = key.split('.') + nested_config = config + for nested_key in nested_keys[:-1]: + nested_config = nested_config.get(nested_key, {}) + nested_config[nested_keys[-1]] = 'REDACTED' + else: + config[key] = 'REDACTED' + + return config diff --git a/freqtrade/configuration/config_setup.py b/freqtrade/configuration/config_setup.py index d49bf61f6..154ba5175 100644 --- a/freqtrade/configuration/config_setup.py +++ b/freqtrade/configuration/config_setup.py @@ -10,7 +10,8 @@ from .configuration import Configuration logger = logging.getLogger(__name__) -def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]: +def setup_utils_configuration( + args: Dict[str, Any], method: RunMode, *, set_dry: bool = True) -> Dict[str, Any]: """ Prepare the configuration for utils subcommands :param args: Cli args from Arguments() @@ -21,7 +22,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str config = configuration.get_config() # Ensure these modes are using Dry-run - config['dry_run'] = True + if set_dry: + config['dry_run'] = True validate_config_consistency(config, preliminary=True) return config diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 4afed60cd..419af8347 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -9,7 +9,7 @@ from jsonschema.exceptions import ValidationError, best_match from freqtrade import constants from freqtrade.configuration.deprecated_settings import process_deprecated_setting from freqtrade.enums import RunMode, TradingMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError logger = logging.getLogger(__name__) @@ -73,7 +73,7 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal Should be ran after loading both configuration and strategy, since strategies can set certain configuration settings too. :param conf: Config in JSON format - :return: Returns None if everything is ok, otherwise throw an OperationalException + :return: Returns None if everything is ok, otherwise throw an ConfigurationError """ # validating trailing stoploss @@ -98,12 +98,12 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal def _validate_unlimited_amount(conf: Dict[str, Any]) -> None: """ If edge is disabled, either max_open_trades or stake_amount need to be set. - :raise: OperationalException if config validation failed + :raise: ConfigurationError if config validation failed """ if (not conf.get('edge', {}).get('enabled') and conf.get('max_open_trades') == float('inf') and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT): - raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.") + raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.") def _validate_price_config(conf: Dict[str, Any]) -> None: @@ -113,18 +113,18 @@ def _validate_price_config(conf: Dict[str, Any]) -> None: # TODO: The below could be an enforced setting when using market orders if (conf.get('order_types', {}).get('entry') == 'market' and conf.get('entry_pricing', {}).get('price_side') not in ('ask', 'other')): - raise OperationalException( + raise ConfigurationError( 'Market entry orders require entry_pricing.price_side = "other".') if (conf.get('order_types', {}).get('exit') == 'market' and conf.get('exit_pricing', {}).get('price_side') not in ('bid', 'other')): - raise OperationalException('Market exit orders require exit_pricing.price_side = "other".') + raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".') def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if conf.get('stoploss') == 0.0: - raise OperationalException( + raise ConfigurationError( 'The config stoploss needs to be different from 0 to avoid problems with sell orders.' ) # Skip if trailing stoploss is not activated @@ -137,17 +137,17 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if tsl_only_offset: if tsl_positive == 0.0: - raise OperationalException( + raise ConfigurationError( 'The config trailing_only_offset_is_reached needs ' 'trailing_stop_positive_offset to be more than 0 in your config.') if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: - raise OperationalException( + raise ConfigurationError( 'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive in your config.') # Fetch again without default if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0: - raise OperationalException( + raise ConfigurationError( 'The config trailing_stop_positive needs to be different from 0 ' 'to avoid problems with sell orders.' ) @@ -162,7 +162,7 @@ def _validate_edge(conf: Dict[str, Any]) -> None: return if not conf.get('use_exit_signal', True): - raise OperationalException( + raise ConfigurationError( "Edge requires `use_exit_signal` to be True, otherwise no sells will happen." ) @@ -178,7 +178,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None: for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]): if (isinstance(pl, dict) and pl.get('method') == 'StaticPairList' and not conf.get('exchange', {}).get('pair_whitelist')): - raise OperationalException("StaticPairList requires pair_whitelist to be set.") + raise ConfigurationError("StaticPairList requires pair_whitelist to be set.") def _validate_protections(conf: Dict[str, Any]) -> None: @@ -188,13 +188,13 @@ def _validate_protections(conf: Dict[str, Any]) -> None: for prot in conf.get('protections', []): if ('stop_duration' in prot and 'stop_duration_candles' in prot): - raise OperationalException( + raise ConfigurationError( "Protections must specify either `stop_duration` or `stop_duration_candles`.\n" f"Please fix the protection {prot.get('method')}" ) if ('lookback_period' in prot and 'lookback_period_candles' in prot): - raise OperationalException( + raise ConfigurationError( "Protections must specify either `lookback_period` or `lookback_period_candles`.\n" f"Please fix the protection {prot.get('method')}" ) @@ -206,7 +206,7 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None: ob_max = ask_strategy.get('order_book_max') if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'): if ob_min != ob_max: - raise OperationalException( + raise ConfigurationError( "Using order_book_max != order_book_min in exit_pricing is no longer supported." "Please pick one value and use `order_book_top` in the future." ) @@ -234,7 +234,7 @@ def _validate_time_in_force(conf: Dict[str, Any]) -> None: time_in_force = conf.get('order_time_in_force', {}) if 'buy' in time_in_force or 'sell' in time_in_force: if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - raise OperationalException( + raise ConfigurationError( "Please migrate your time_in_force settings to use 'entry' and 'exit'.") else: logger.warning( @@ -255,7 +255,7 @@ def _validate_order_types(conf: Dict[str, Any]) -> None: 'forcesell', 'emergencyexit', 'forceexit', 'forceentry'] if any(x in order_types for x in old_order_types): if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - raise OperationalException( + raise ConfigurationError( "Please migrate your order_types settings to use the new wording.") else: logger.warning( @@ -280,7 +280,7 @@ def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None: unfilledtimeout = conf.get('unfilledtimeout', {}) if any(x in unfilledtimeout for x in ['buy', 'sell']): if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - raise OperationalException( + raise ConfigurationError( "Please migrate your unfilledtimeout settings to use the new wording.") else: @@ -300,7 +300,7 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None: if conf.get('ask_strategy') or conf.get('bid_strategy'): if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - raise OperationalException( + raise ConfigurationError( "Please migrate your pricing settings to use the new wording.") else: @@ -331,7 +331,7 @@ def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None: freqai_enabled = conf.get('freqai', {}).get('enabled', False) analyze_per_epoch = conf.get('analyze_per_epoch', False) if analyze_per_epoch and freqai_enabled: - raise OperationalException( + raise ConfigurationError( 'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') @@ -350,7 +350,7 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) if tf_s < main_tf_s: offending_lines.append(tf) if offending_lines: - raise OperationalException( + raise ConfigurationError( f"Main timeframe of {main_tf} must be smaller or equal to FreqAI " f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}") @@ -368,17 +368,17 @@ def _validate_freqai_backtest(conf: Dict[str, Any]) -> None: timerange = conf.get('timerange') freqai_backtest_live_models = conf.get('freqai_backtest_live_models', False) if freqai_backtest_live_models and freqai_enabled and timerange: - raise OperationalException( + raise ConfigurationError( 'Using timerange parameter is not supported with ' '--freqai-backtest-live-models parameter.') if freqai_backtest_live_models and not freqai_enabled: - raise OperationalException( + raise ConfigurationError( 'Using --freqai-backtest-live-models parameter is only ' 'supported with a FreqAI strategy.') if freqai_enabled and not freqai_backtest_live_models and not timerange: - raise OperationalException( + raise ConfigurationError( 'Please pass --timerange if you intend to use FreqAI for backtesting.') @@ -386,12 +386,12 @@ def _validate_consumers(conf: Dict[str, Any]) -> None: emc_conf = conf.get('external_message_consumer', {}) if emc_conf.get('enabled', False): if len(emc_conf.get('producers', [])) < 1: - raise OperationalException("You must specify at least 1 Producer to connect to.") + raise ConfigurationError("You must specify at least 1 Producer to connect to.") producer_names = [p['name'] for p in emc_conf.get('producers', [])] duplicates = [item for item, count in Counter(producer_names).items() if count > 1] if duplicates: - raise OperationalException( + raise ConfigurationError( f"Producer names must be unique. Duplicate: {', '.join(duplicates)}") if conf.get('process_only_new_candles', True): # Warning here or require it? diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 58b1fdf84..0da307175 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -200,6 +200,12 @@ class Configuration: config['exportfilename'] = (config['user_data_dir'] / 'backtest_results') + if self.args.get('show_sensitive'): + logger.warning( + "Sensitive information will be shown in the upcomming output. " + "Please make sure to never share this output without redacting " + "the information yourself.") + def _process_optimize_options(self, config: Config) -> None: # This will override the strategy configuration diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 46c19a5b2..6a2d365a3 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -6,7 +6,7 @@ import logging from typing import Optional from freqtrade.constants import Config -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def process_removed_setting(config: Config, section1_config = config.get(section1, {}) if name1 in section1_config: section_2 = f"{section2}.{name2}" if section2 else f"{name2}" - raise OperationalException( + raise ConfigurationError( f"Setting `{section1}.{name1}` has been moved to `{section_2}. " f"Please delete it from your configuration and use the `{section_2}` " "setting instead." @@ -122,7 +122,7 @@ def process_temporary_deprecated_settings(config: Config) -> None: None, 'ignore_roi_if_entry_signal') if (config.get('edge', {}).get('enabled', False) and 'capital_available_percentage' in config.get('edge', {})): - raise OperationalException( + raise ConfigurationError( "DEPRECATED: " "Using 'edge.capital_available_percentage' has been deprecated in favor of " "'tradable_balance_ratio'. Please migrate your configuration to " @@ -131,7 +131,7 @@ def process_temporary_deprecated_settings(config: Config) -> None: ) if 'ticker_interval' in config: - raise OperationalException( + raise ConfigurationError( "DEPRECATED: 'ticker_interval' detected. " "Please use 'timeframe' instead of 'ticker_interval." ) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 57424468d..22eeeca55 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional import rapidjson from freqtrade.constants import MINIMAL_CONFIG, Config -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.misc import deep_merge_dicts @@ -46,7 +46,7 @@ def load_file(path: Path) -> Dict[str, Any]: with path.open('r') as file: config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE) except FileNotFoundError: - raise OperationalException(f'File "{path}" not found!') + raise OperationalException(f'File "{path}" not found!') from None return config @@ -63,10 +63,10 @@ def load_config_file(path: str) -> Dict[str, Any]: except FileNotFoundError: raise OperationalException( f'Config file "{path}" not found!' - ' Please create a config file or check whether it exists.') + ' Please create a config file or check whether it exists.') from None except rapidjson.JSONDecodeError as e: err_range = log_config_error_range(path, str(e)) - raise OperationalException( + raise ConfigurationError( f'{e}\n' f'Please verify the following segment of your configuration:\n{err_range}' if err_range else 'Please verify your configuration file for syntax errors.' @@ -83,7 +83,7 @@ def load_from_files( """ config: Config = {} if level > 5: - raise OperationalException("Config loop detected.") + raise ConfigurationError("Config loop detected.") if not files: return deepcopy(MINIMAL_CONFIG) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 23e3a6b60..b82b13b10 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -9,7 +9,7 @@ from typing import Optional from typing_extensions import Self from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError logger = logging.getLogger(__name__) @@ -156,7 +156,7 @@ class TimeRange: else: stop = int(stops) if start > stop > 0: - raise OperationalException( + raise ConfigurationError( f'Start date is after stop date for timerange "{text}"') return cls(stype[0], stype[1], start, stop) - raise OperationalException(f'Incorrect syntax for timerange "{text}"') + raise ConfigurationError(f'Incorrect syntax for timerange "{text}"') diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 9d9b17552..ef92d4db6 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -11,7 +11,7 @@ import numpy as np import pandas as pd from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.misc import file_dump_json, json_load from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename from freqtrade.persistence import LocalTrade, Trade, init_db @@ -106,7 +106,7 @@ def get_latest_hyperopt_file( directory = Path(directory) if predef_filename: if Path(predef_filename).is_absolute(): - raise OperationalException( + raise ConfigurationError( "--hyperopt-filename expects only the filename, not an absolute path.") return directory / predef_filename return directory / get_latest_hyperopt_filename(directory) diff --git a/freqtrade/data/converter/converter.py b/freqtrade/data/converter/converter.py index 0db947e35..0ebf24a4f 100644 --- a/freqtrade/data/converter/converter.py +++ b/freqtrade/data/converter/converter.py @@ -200,7 +200,7 @@ def convert_ohlcv_format( :param convert_to: Target format :param erase: Erase source data (does not apply if source and target format are identical) """ - from freqtrade.data.history.idatahandler import get_datahandler + from freqtrade.data.history import get_datahandler src = get_datahandler(config['datadir'], convert_from) trg = get_datahandler(config['datadir'], convert_to) timeframes = config.get('timeframes', [config.get('timeframe')]) diff --git a/freqtrade/data/converter/trade_converter.py b/freqtrade/data/converter/trade_converter.py index bd4efb77e..a7cc97cb8 100644 --- a/freqtrade/data/converter/trade_converter.py +++ b/freqtrade/data/converter/trade_converter.py @@ -11,7 +11,7 @@ from pandas import DataFrame, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES, Config, TradeList) -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from freqtrade.exceptions import OperationalException @@ -88,25 +88,23 @@ def convert_trades_to_ohlcv( timeframes: List[str], datadir: Path, timerange: TimeRange, - erase: bool = False, - data_format_ohlcv: str = 'feather', - data_format_trades: str = 'feather', - candle_type: CandleType = CandleType.SPOT + erase: bool, + data_format_ohlcv: str, + data_format_trades: str, + candle_type: CandleType, ) -> None: """ Convert stored trades data to ohlcv data """ - from freqtrade.data.history.idatahandler import get_datahandler + from freqtrade.data.history import get_datahandler data_handler_trades = get_datahandler(datadir, data_format=data_format_trades) data_handler_ohlcv = get_datahandler(datadir, data_format=data_format_ohlcv) - if not pairs: - pairs = data_handler_trades.trades_get_pairs(datadir) logger.info(f"About to convert pairs: '{', '.join(pairs)}', " f"intervals: '{', '.join(timeframes)}' to {datadir}") - + trading_mode = TradingMode.FUTURES if candle_type != CandleType.SPOT else TradingMode.SPOT for pair in pairs: - trades = data_handler_trades.trades_load(pair) + trades = data_handler_trades.trades_load(pair, trading_mode) for timeframe in timeframes: if erase: if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type): @@ -116,7 +114,7 @@ def convert_trades_to_ohlcv( # Store ohlcv data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type) except ValueError: - logger.exception(f'Could not convert {pair} to OHLCV.') + logger.warning(f'Could not convert {pair} to OHLCV.') def convert_trades_format(config: Config, convert_from: str, convert_to: str, erase: bool): @@ -137,18 +135,19 @@ def convert_trades_format(config: Config, convert_from: str, convert_to: str, er import_kraken_trades_from_csv(config, convert_to) return - from freqtrade.data.history.idatahandler import get_datahandler + from freqtrade.data.history import get_datahandler src = get_datahandler(config['datadir'], convert_from) trg = get_datahandler(config['datadir'], convert_to) if 'pairs' not in config: config['pairs'] = src.trades_get_pairs(config['datadir']) logger.info(f"Converting trades for {config['pairs']}") - + trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) for pair in config['pairs']: - data = src.trades_load(pair=pair) + data = src.trades_load(pair, trading_mode) logger.info(f"Converting {len(data)} trades for {pair}") - trg.trades_store(pair, data) + trg.trades_store(pair, data, trading_mode) + if erase and convert_from != convert_to: logger.info(f"Deleting source Trade data for {pair}.") - src.trades_purge(pair=pair) + src.trades_purge(pair, trading_mode) diff --git a/freqtrade/data/converter/trade_converter_kraken.py b/freqtrade/data/converter/trade_converter_kraken.py index b0fa11c25..90f7df28f 100644 --- a/freqtrade/data/converter/trade_converter_kraken.py +++ b/freqtrade/data/converter/trade_converter_kraken.py @@ -6,7 +6,8 @@ import pandas as pd from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, Config from freqtrade.data.converter.trade_converter import (trades_convert_types, trades_df_remove_duplicates) -from freqtrade.data.history.idatahandler import get_datahandler +from freqtrade.data.history import get_datahandler +from freqtrade.enums import TradingMode from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import ExchangeResolver @@ -79,4 +80,4 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str): f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to " f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}") - data_handler.trades_store(pair, trades_df) + data_handler.trades_store(pair, trades_df, TradingMode.SPOT) diff --git a/freqtrade/data/history/__init__.py b/freqtrade/data/history/__init__.py index 414848c22..f989fd801 100644 --- a/freqtrade/data/history/__init__.py +++ b/freqtrade/data/history/__init__.py @@ -6,7 +6,7 @@ Includes: * download data from exchange and store to disk """ # flake8: noqa: F401 +from .datahandlers import get_datahandler from .history_utils import (convert_trades_to_ohlcv, download_data_main, get_timerange, load_data, load_pair_history, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, refresh_data, validate_backtest_data) -from .idatahandler import get_datahandler diff --git a/freqtrade/data/history/datahandlers/__init__.py b/freqtrade/data/history/datahandlers/__init__.py new file mode 100644 index 000000000..d76c6aada --- /dev/null +++ b/freqtrade/data/history/datahandlers/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from .idatahandler import IDataHandler, get_datahandler diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/datahandlers/featherdatahandler.py similarity index 90% rename from freqtrade/data/history/featherdatahandler.py rename to freqtrade/data/history/datahandlers/featherdatahandler.py index 44d337836..6d57dbed7 100644 --- a/freqtrade/data/history/featherdatahandler.py +++ b/freqtrade/data/history/datahandlers/featherdatahandler.py @@ -5,7 +5,7 @@ from pandas import DataFrame, read_feather, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from .idatahandler import IDataHandler @@ -82,14 +82,15 @@ class FeatherDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_store(self, pair: str, data: DataFrame) -> None: + def _trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) self.create_dir_if_needed(filename) data.reset_index(drop=True).to_feather(filename, compression_level=9, compression='lz4') @@ -102,15 +103,18 @@ class FeatherDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json # TODO: respect timerange ... :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: Dataframe containing trades """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) if not filename.exists(): return DataFrame(columns=DEFAULT_TRADES_COLUMNS) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/datahandlers/hdf5datahandler.py similarity index 90% rename from freqtrade/data/history/hdf5datahandler.py rename to freqtrade/data/history/datahandlers/hdf5datahandler.py index d22fd9e31..cb2cdd884 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/datahandlers/hdf5datahandler.py @@ -6,7 +6,7 @@ import pandas as pd from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from .idatahandler import IDataHandler @@ -35,7 +35,7 @@ class HDF5DataHandler(IDataHandler): self.create_dir_if_needed(filename) _data.loc[:, self._columns].to_hdf( - filename, key, mode='a', complevel=9, complib='blosc', + filename, key=key, mode='a', complevel=9, complib='blosc', format='table', data_columns=['date'] ) @@ -100,17 +100,18 @@ class HDF5DataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_store(self, pair: str, data: pd.DataFrame) -> None: + def _trades_store(self, pair: str, data: pd.DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ key = self._pair_trades_key(pair) data.to_hdf( - self._pair_trades_filename(self._datadir, pair), key, + self._pair_trades_filename(self._datadir, pair, trading_mode), key=key, mode='a', complevel=9, complib='blosc', format='table', data_columns=['timestamp'] ) @@ -124,15 +125,18 @@ class HDF5DataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> pd.DataFrame: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> pd.DataFrame: """ Load a pair from h5 file. :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: Dataframe containing trades """ key = self._pair_trades_key(pair) - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) if not filename.exists(): return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/datahandlers/idatahandler.py similarity index 93% rename from freqtrade/data/history/idatahandler.py rename to freqtrade/data/history/datahandlers/idatahandler.py index 47c2dd838..fbaded640 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/datahandlers/idatahandler.py @@ -172,12 +172,13 @@ class IDataHandler(ABC): return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match] @abstractmethod - def _trades_store(self, pair: str, data: DataFrame) -> None: + def _trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ @abstractmethod @@ -190,45 +191,55 @@ class IDataHandler(ABC): """ @abstractmethod - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: Dataframe containing trades """ - def trades_store(self, pair: str, data: DataFrame) -> None: + def trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ # Filter on expected columns (will remove the actual date column). - self._trades_store(pair, data[DEFAULT_TRADES_COLUMNS]) + self._trades_store(pair, data[DEFAULT_TRADES_COLUMNS], trading_mode) - def trades_purge(self, pair: str) -> bool: + def trades_purge(self, pair: str, trading_mode: TradingMode) -> bool: """ Remove data for this pair :param pair: Delete data for this pair. + :param trading_mode: Trading mode to use (used to determine the filename) :return: True when deleted, false if file did not exist. """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) if filename.exists(): filename.unlink() return True return False - def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame: + def trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json Removes duplicates in the process. :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: List of trades """ - trades = trades_df_remove_duplicates(self._trades_load(pair, timerange=timerange)) + trades = trades_df_remove_duplicates( + self._trades_load(pair, trading_mode, timerange=timerange) + ) trades = trades_convert_types(trades) return trades @@ -264,8 +275,12 @@ class IDataHandler(ABC): return filename @classmethod - def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path: + def _pair_trades_filename(cls, datadir: Path, pair: str, trading_mode: TradingMode) -> Path: pair_s = misc.pair_to_filename(pair) + if trading_mode == TradingMode.FUTURES: + # Futures pair ... + datadir = datadir.joinpath('futures') + filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}') return filename diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/datahandlers/jsondatahandler.py similarity index 90% rename from freqtrade/data/history/jsondatahandler.py rename to freqtrade/data/history/datahandlers/jsondatahandler.py index 9a02a7769..2d0333fed 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/datahandlers/jsondatahandler.py @@ -8,7 +8,7 @@ from freqtrade import misc from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS from freqtrade.data.converter import trades_dict_to_list, trades_list_to_df -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from .idatahandler import IDataHandler @@ -37,7 +37,7 @@ class JsonDataHandler(IDataHandler): self.create_dir_if_needed(filename) _data = data.copy() # Convert date to int - _data['date'] = _data['date'].view(np.int64) // 1000 // 1000 + _data['date'] = _data['date'].astype(np.int64) // 1000 // 1000 # Reset index, select only appropriate columns and save as json _data.reset_index(drop=True).loc[:, self._columns].to_json( @@ -94,14 +94,15 @@ class JsonDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_store(self, pair: str, data: DataFrame) -> None: + def _trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) trades = data.values.tolist() misc.file_dump_json(filename, trades, is_zip=self._use_zip) @@ -114,15 +115,18 @@ class JsonDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json # TODO: respect timerange ... :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: Dataframe containing trades """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) tradesdata = misc.file_load_json(filename) if not tradesdata: diff --git a/freqtrade/data/history/parquetdatahandler.py b/freqtrade/data/history/datahandlers/parquetdatahandler.py similarity index 90% rename from freqtrade/data/history/parquetdatahandler.py rename to freqtrade/data/history/datahandlers/parquetdatahandler.py index c0b0cad63..01becdc84 100644 --- a/freqtrade/data/history/parquetdatahandler.py +++ b/freqtrade/data/history/datahandlers/parquetdatahandler.py @@ -4,8 +4,8 @@ from typing import Optional from pandas import DataFrame, read_parquet, to_datetime from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList -from freqtrade.enums import CandleType +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS +from freqtrade.enums import CandleType, TradingMode from .idatahandler import IDataHandler @@ -81,14 +81,15 @@ class ParquetDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_store(self, pair: str, data: DataFrame) -> None: + def _trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) self.create_dir_if_needed(filename) data.reset_index(drop=True).to_parquet(filename) @@ -101,15 +102,18 @@ class ParquetDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json # TODO: respect timerange ... :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: List of trades """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) if not filename.exists(): return DataFrame(columns=DEFAULT_TRADES_COLUMNS) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index ff6c2561d..e9ff2b2df 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -12,8 +12,8 @@ from freqtrade.constants import (DATETIME_PRINT_FORMAT, DEFAULT_DATAFRAME_COLUMN from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_ohlcv, ohlcv_to_dataframe, trades_df_remove_duplicates, trades_list_to_df) -from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler -from freqtrade.enums import CandleType +from freqtrade.data.history.datahandlers import IDataHandler, get_datahandler +from freqtrade.enums import CandleType, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist @@ -333,7 +333,8 @@ def _download_trades_history(exchange: Exchange, pair: str, *, new_pairs_days: int = 30, timerange: Optional[TimeRange] = None, - data_handler: IDataHandler + data_handler: IDataHandler, + trading_mode: TradingMode, ) -> bool: """ Download trade history from the exchange. @@ -349,7 +350,7 @@ def _download_trades_history(exchange: Exchange, if timerange.stoptype == 'date': until = timerange.stopts * 1000 - trades = data_handler.trades_load(pair) + trades = data_handler.trades_load(pair, trading_mode) # TradesList columns are defined in constants.DEFAULT_TRADES_COLUMNS # DEFAULT_TRADES_COLUMNS: 0 -> timestamp @@ -388,7 +389,7 @@ def _download_trades_history(exchange: Exchange, trades = concat([trades, new_trades_df], axis=0) # Remove duplicates to make sure we're not storing data we don't need trades = trades_df_remove_duplicates(trades) - data_handler.trades_store(pair, data=trades) + data_handler.trades_store(pair, trades, trading_mode) logger.debug("New Start: %s", 'None' if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}") @@ -405,8 +406,10 @@ def _download_trades_history(exchange: Exchange, def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, - timerange: TimeRange, new_pairs_days: int = 30, - erase: bool = False, data_format: str = 'feather') -> List[str]: + timerange: TimeRange, trading_mode: TradingMode, + new_pairs_days: int = 30, + erase: bool = False, data_format: str = 'feather', + ) -> List[str]: """ Refresh stored trades data for backtesting and hyperopt operations. Used by freqtrade download-data subcommand. @@ -421,7 +424,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: continue if erase: - if data_handler.trades_purge(pair): + if data_handler.trades_purge(pair, trading_mode): logger.info(f'Deleting existing data for pair {pair}.') logger.info(f'Downloading trades for pair {pair}.') @@ -429,7 +432,8 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: pair=pair, new_pairs_days=new_pairs_days, timerange=timerange, - data_handler=data_handler) + data_handler=data_handler, + trading_mode=trading_mode) return pairs_not_available @@ -516,12 +520,12 @@ def download_data_main(config: Config) -> None: # Start downloading try: if config.get('download_trades'): - if config.get('trading_mode') == 'futures': - raise OperationalException("Trade download not supported for futures.") pairs_not_available = refresh_backtest_trades_data( exchange, pairs=expanded_pairs, datadir=config['datadir'], timerange=timerange, new_pairs_days=config['new_pairs_days'], - erase=bool(config.get('erase')), data_format=config['dataformat_trades']) + erase=bool(config.get('erase')), data_format=config['dataformat_trades'], + trading_mode=config.get('trading_mode', TradingMode.SPOT), + ) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( @@ -529,6 +533,7 @@ def download_data_main(config: Config) -> None: datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format_ohlcv=config['dataformat_ohlcv'], data_format_trades=config['dataformat_trades'], + candle_type=config.get('candle_type_def', CandleType.SPOT), ) else: if not exchange.get_option('ohlcv_has_history', True): diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 6b0039a3f..8b2c9c993 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -12,6 +12,12 @@ class OperationalException(FreqtradeException): """ +class ConfigurationError(OperationalException): + """ + Configuration error. Usually caused by invalid configuration. + """ + + class DependencyException(FreqtradeException): """ Indicates that an assumed dependency is not met. diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 145332a33..1c6ba9cbd 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -15,10 +15,12 @@ from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_c contracts_to_amount, date_minus_candles, is_exchange_known_ccxt, list_available_exchanges, market_is_active, price_to_precision, - timeframe_to_minutes, timeframe_to_msecs, - timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_resample_freq, timeframe_to_seconds, validate_exchange) +from freqtrade.exchange.exchange_utils_timeframe import (timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, + timeframe_to_prev_date, + timeframe_to_resample_freq, + timeframe_to_seconds) from freqtrade.exchange.gate import Gate from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.htx import Htx diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 36c2e5996..09076c570 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -341,6 +341,136 @@ } } ], + "1000PEPE/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDC", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDC", + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.025", + "cum": "75.0" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "minNotional": 50000.0, + "maxNotional": 1200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1200000", + "notionalFloor": "50000", + "maintMarginRatio": "0.05", + "cum": "1325.0" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "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": "61325.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 3600000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "3600000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "136325.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 3600000.0, + "maxNotional": 9000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "9000000", + "notionalFloor": "3600000", + "maintMarginRatio": "0.25", + "cum": "586325.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "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": "2836325.0" + } + } + ], "1000PEPE/USDT:USDT": [ { "tier": 1.0, @@ -606,96 +736,258 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "50000", + "initialLeverage": "25", + "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 25000.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "200000", - "notionalFloor": "50000", - "maintMarginRatio": "0.05", - "cum": "1300.0" + "initialLeverage": "20", + "notionalCap": "80000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.1", - "cum": "11300.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.125", - "cum": "23800.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.25", - "cum": "148800.0" + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "82150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 3000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.5", + "cum": "1332150.0" + } + } + ], + "1000SHIB/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "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": "USDC", + "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": "USDC", + "minNotional": 25000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "150000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "67.5" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "minNotional": 150000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "600000", + "notionalFloor": "150000", + "maintMarginRatio": "0.025", + "cum": "2317.5" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "minNotional": 600000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "1500000", + "notionalFloor": "600000", + "maintMarginRatio": "0.05", + "cum": "17317.5" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.1", + "cum": "92317.5" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", "notionalCap": "5000000", "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "167317.5" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 5000000.0, + "maxNotional": 9000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "9000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "792317.5" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 9000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "9000000", "maintMarginRatio": "0.5", - "cum": "898800.0" + "cum": "3042317.5" } } ], @@ -1193,95 +1485,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "50000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 50000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "500000", + "notionalFloor": "50000", + "maintMarginRatio": "0.05", + "cum": "1400.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "26400.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1000000.0, + "maxNotional": 1250000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "1250000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "51400.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 1250000.0, + "maxNotional": 2500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "2500000", + "notionalFloor": "1250000", + "maintMarginRatio": "0.25", + "cum": "207650.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 2500000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "832650.0" } } ], @@ -1545,6 +1853,120 @@ } } ], + "AEVO/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "AGIX/USDT:USDT": [ { "tier": 1.0, @@ -1795,95 +2217,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "80000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "82150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "1332150.0" } } ], @@ -2219,95 +2657,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "80000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "82150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "1332150.0" } } ], @@ -2546,10 +3000,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.015, - "maxLeverage": 50.0, + "maxLeverage": 21.0, "info": { "bracket": "1", - "initialLeverage": "50", + "initialLeverage": "21", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.015", @@ -2624,13 +3078,13 @@ "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "3000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386925.0" @@ -3960,96 +4414,112 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 20000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "20000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 20000.0, + "maxNotional": 30000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "initialLeverage": "20", + "notionalCap": "30000", + "notionalFloor": "20000", + "maintMarginRatio": "0.025", + "cum": "125.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 30000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "300000", + "notionalFloor": "30000", + "maintMarginRatio": "0.05", + "cum": "875.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 300000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "600000", + "notionalFloor": "300000", + "maintMarginRatio": "0.1", + "cum": "15875.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 600000.0, + "maxNotional": 750000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "750000", + "notionalFloor": "600000", + "maintMarginRatio": "0.125", + "cum": "30875.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 750000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "1500000", + "notionalFloor": "750000", + "maintMarginRatio": "0.25", + "cum": "124625.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "3000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "499625.0" } } ], @@ -4151,6 +4621,168 @@ } } ], + "AVAX/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "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": "USDC", + "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": "USDC", + "minNotional": 10000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.0075, + "maxLeverage": 40.0, + "info": { + "bracket": "3", + "initialLeverage": "40", + "notionalCap": "25000", + "notionalFloor": "10000", + "maintMarginRatio": "0.0075", + "cum": "17.5" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "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": "USDC", + "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": "USDC", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "6", + "initialLeverage": "10", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.05", + "cum": "7080.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "7", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "57080.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "8", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "107080.0" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "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": "USDC", + "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" + } + } + ], "AVAX/USDT:USDT": [ { "tier": 1.0, @@ -4313,6 +4945,120 @@ } } ], + "AXL/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "AXS/USDT:USDT": [ { "tier": 1.0, @@ -5606,10 +6352,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 11.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "11", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.01", @@ -5684,13 +6430,13 @@ "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386950.0" @@ -6461,6 +7207,120 @@ } } ], + "BOME/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "BOND/USDT:USDT": [ { "tier": 1.0, @@ -7013,7 +7873,7 @@ } } ], - "BTC/USDT:USDT-240329": [ + "BTC/USDT:USDT-240628": [ { "tier": 1.0, "currency": "USDT", @@ -7143,7 +8003,7 @@ } } ], - "BTC/USDT:USDT-240628": [ + "BTC/USDT:USDT-240927": [ { "tier": 1.0, "currency": "USDT", @@ -9008,10 +9868,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 11.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "11", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -9086,13 +9946,13 @@ "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "3000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -9922,10 +10782,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 11.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "11", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -10000,13 +10860,13 @@ "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "3000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -10841,6 +11701,136 @@ } } ], + "DYM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "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": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "80000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "150.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.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": "82150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.5", + "cum": "1332150.0" + } + } + ], "EDU/USDT:USDT": [ { "tier": 1.0, @@ -12139,7 +13129,7 @@ } } ], - "ETH/USDT:USDT-240329": [ + "ETH/USDT:USDT-240628": [ { "tier": 1.0, "currency": "USDT", @@ -12269,7 +13259,7 @@ } } ], - "ETH/USDT:USDT-240628": [ + "ETH/USDT:USDT-240927": [ { "tier": 1.0, "currency": "USDT", @@ -12399,6 +13389,120 @@ } } ], + "ETHFI/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "ETHW/USDT:USDT": [ { "tier": 1.0, @@ -12976,10 +14080,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 11.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "11", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -13054,13 +14158,13 @@ "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -13833,6 +14937,120 @@ } } ], + "GLM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "GLMR/USDT:USDT": [ { "tier": 1.0, @@ -16235,14 +17453,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.015", "cum": "0.0" } }, @@ -16251,14 +17469,14 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "25.0" } }, @@ -16266,64 +17484,96 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "80000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "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": "82150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "1332150.0" } } ], @@ -16577,95 +17827,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "80000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "82150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "1332150.0" } } ], @@ -17679,6 +18945,168 @@ } } ], + "LINK/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "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": "USDC", + "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": "USDC", + "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": "USDC", + "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": "USDC", + "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": "USDC", + "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": "USDC", + "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": "USDC", + "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": "USDC", + "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": "USDC", + "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" + } + } + ], "LINK/USDT:USDT": [ { "tier": 1.0, @@ -18955,14 +20383,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.015, + "maintenanceMarginRate": 0.01, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.015", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -18970,96 +20398,112 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "50000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "50.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, + "minNotional": 50000.0, "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "300.0" } }, { "tier": 4.0, "currency": "USDT", "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", + "initialLeverage": "10", + "notionalCap": "1000000", "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "maintMarginRatio": "0.05", + "cum": "2800.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "52800.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 2000000.0, + "maxNotional": 2500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "2500000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "102800.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2500000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "5000000", + "notionalFloor": "2500000", + "maintMarginRatio": "0.25", + "cum": "415300.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "10000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "1665300.0" } } ], @@ -19453,6 +20897,120 @@ } } ], + "MAVIA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "MBL/USDT:USDT": [ { "tier": 1.0, @@ -19682,6 +21240,136 @@ } ], "MEME/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "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": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "80000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "150.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.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": "82150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.5", + "cum": "1332150.0" + } + } + ], + "METIS/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -20267,6 +21955,120 @@ } } ], + "MYRO/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "NEAR/USDT:USDT": [ { "tier": 1.0, @@ -20549,95 +22351,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "50000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 50000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "500000", + "notionalFloor": "50000", + "maintMarginRatio": "0.05", + "cum": "1400.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "26400.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1000000.0, + "maxNotional": 1250000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "1250000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "51400.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 1250000.0, + "maxNotional": 2500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "2500000", + "notionalFloor": "1250000", + "maintMarginRatio": "0.25", + "cum": "207650.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 2500000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "832650.0" } } ], @@ -21163,6 +22981,120 @@ } } ], + "OM/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "OMG/USDT:USDT": [ { "tier": 1.0, @@ -21299,95 +23231,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "80000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "82150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "1332150.0" } } ], @@ -21961,6 +23909,168 @@ } } ], + "ORDI/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.0065", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDC", + "minNotional": 5000.0, + "maxNotional": 75000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "75000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "17.5" + } + }, + { + "tier": 3.0, + "currency": "USDC", + "minNotional": 75000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 40.0, + "info": { + "bracket": "3", + "initialLeverage": "40", + "notionalCap": "150000", + "notionalFloor": "75000", + "maintMarginRatio": "0.015", + "cum": "392.5" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "minNotional": 150000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "4", + "initialLeverage": "25", + "notionalCap": "300000", + "notionalFloor": "150000", + "maintMarginRatio": "0.02", + "cum": "1142.5" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "minNotional": 300000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "5", + "initialLeverage": "20", + "notionalCap": "1000000", + "notionalFloor": "300000", + "maintMarginRatio": "0.025", + "cum": "2642.5" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "6", + "initialLeverage": "10", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.05", + "cum": "27642.5" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "7", + "initialLeverage": "5", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.1", + "cum": "177642.5" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "8", + "initialLeverage": "4", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.125", + "cum": "327642.5" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.25", + "cum": "1577642.5" + } + }, + { + "tier": 10.0, + "currency": "USDC", + "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "10", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.5", + "cum": "6577642.5" + } + } + ], "ORDI/USDT:USDT": [ { "tier": 1.0, @@ -22677,6 +24787,136 @@ } } ], + "PIXEL/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "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": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "80000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "150.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.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": "82150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "8000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.5", + "cum": "1332150.0" + } + } + ], "POLYX/USDT:USDT": [ { "tier": 1.0, @@ -22791,6 +25031,120 @@ } } ], + "PORTAL/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "POWR/USDT:USDT": [ { "tier": 1.0, @@ -24111,6 +26465,120 @@ } } ], + "RONIN/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "ROSE/USDT:USDT": [ { "tier": 1.0, @@ -26658,10 +29126,10 @@ "minNotional": 0.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.03, - "maxLeverage": 20.0, + "maxLeverage": 11.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "11", "notionalCap": "25000", "notionalFloor": "0", "maintMarginRatio": "0.03", @@ -26749,6 +29217,120 @@ } } ], + "STRK/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "STX/USDT:USDT": [ { "tier": 1.0, @@ -26863,6 +29445,136 @@ } } ], + "SUI/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "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": "USDC", + "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": "USDC", + "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": "300.0" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "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": "15300.0" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "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": "75300.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "4000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "150300.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 4000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.25", + "cum": "650300.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "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": "2150300.0" + } + } + ], "SUI/USDT:USDT": [ { "tier": 1.0, @@ -27569,14 +30281,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.015, + "maintenanceMarginRate": 0.01, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.015", + "maintMarginRatio": "0.01", "cum": "0.0" } }, @@ -27584,96 +30296,112 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "50000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "50.0" } }, { "tier": 3.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": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "initialLeverage": "20", + "notionalCap": "150000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "300.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 150000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "1500000", + "notionalFloor": "150000", + "maintMarginRatio": "0.05", + "cum": "4050.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 1500000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "3000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.1", + "cum": "79050.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 3000000.0, + "maxNotional": 3750000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "3750000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "154050.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 3750000.0, + "maxNotional": 7500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "7500000", + "notionalFloor": "3750000", + "maintMarginRatio": "0.25", + "cum": "622800.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 7500000.0, + "maxNotional": 15000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "15000000", + "notionalFloor": "7500000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "2497800.0" } } ], @@ -28003,6 +30731,120 @@ } } ], + "TON/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "TRB/USDT:USDT": [ { "tier": 1.0, @@ -29109,6 +31951,120 @@ } } ], + "VANRY/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "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, + "info": { + "bracket": "2", + "initialLeverage": "20", + "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": 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": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "VET/USDT:USDT": [ { "tier": 1.0, @@ -29489,95 +32445,257 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "80000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "82150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "1332150.0" + } + } + ], + "WLD/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDC", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDC", + "minNotional": 25000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "150000", + "notionalFloor": "25000", + "maintMarginRatio": "0.02", + "cum": "270.0" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "minNotional": 150000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "300000", + "notionalFloor": "150000", + "maintMarginRatio": "0.025", + "cum": "1020.0" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "minNotional": 300000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "3000000", + "notionalFloor": "300000", + "maintMarginRatio": "0.05", + "cum": "8520.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.1", + "cum": "158520.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 6000000.0, + "maxNotional": 7500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "7500000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.125", + "cum": "308520.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 7500000.0, + "maxNotional": 15000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "15000000", + "notionalFloor": "7500000", + "maintMarginRatio": "0.25", + "cum": "1246020.0" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 15000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "15000000", + "maintMarginRatio": "0.5", + "cum": "4996020.0" } } ], @@ -29587,14 +32705,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 50.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", + "initialLeverage": "75", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.006", "cum": "0.0" } }, @@ -29603,111 +32721,127 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "25", + "initialLeverage": "50", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.02", - "cum": "50.0" + "maintMarginRatio": "0.01", + "cum": "20.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "3", - "initialLeverage": "20", - "notionalCap": "50000", + "initialLeverage": "25", + "notionalCap": "150000", "notionalFloor": "25000", - "maintMarginRatio": "0.025", - "cum": "175.0" + "maintMarginRatio": "0.02", + "cum": "270.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 400000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 150000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "10", - "notionalCap": "400000", - "notionalFloor": "50000", - "maintMarginRatio": "0.05", - "cum": "1425.0" + "initialLeverage": "20", + "notionalCap": "300000", + "notionalFloor": "150000", + "maintMarginRatio": "0.025", + "cum": "1020.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 400000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 300000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "400000", - "maintMarginRatio": "0.1", - "cum": "21425.0" + "initialLeverage": "10", + "notionalCap": "3000000", + "notionalFloor": "300000", + "maintMarginRatio": "0.05", + "cum": "8520.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "6", - "initialLeverage": "4", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.125", - "cum": "46425.0" + "initialLeverage": "5", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.1", + "cum": "158520.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 6000000.0, + "maxNotional": 7500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "7", - "initialLeverage": "2", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.25", - "cum": "296425.0" + "initialLeverage": "4", + "notionalCap": "7500000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.125", + "cum": "308520.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 7500000.0, + "maxNotional": 15000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "15000000", + "notionalFloor": "7500000", + "maintMarginRatio": "0.25", + "cum": "1246020.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 15000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "8", + "bracket": "9", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "30000000", + "notionalFloor": "15000000", "maintMarginRatio": "0.5", - "cum": "1546425.0" + "cum": "4996020.0" } } ], @@ -29831,95 +32965,111 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 80000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "20", + "notionalCap": "80000", "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "maintMarginRatio": "0.025", + "cum": "150.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 80000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "80000", + "maintMarginRatio": "0.05", + "cum": "2150.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "1600000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "42150.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", + "maintMarginRatio": "0.125", + "cum": "82150.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "332150.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "1332150.0" } } ], @@ -31424,96 +34574,112 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxNotional": 20000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "20", - "notionalCap": "25000", + "initialLeverage": "25", + "notionalCap": "20000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 20000.0, + "maxNotional": 30000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "675.0" + "initialLeverage": "20", + "notionalCap": "30000", + "notionalFloor": "20000", + "maintMarginRatio": "0.025", + "cum": "125.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 30000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5675.0" + "initialLeverage": "10", + "notionalCap": "300000", + "notionalFloor": "30000", + "maintMarginRatio": "0.05", + "cum": "875.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 300000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.125", - "cum": "10675.0" + "initialLeverage": "5", + "notionalCap": "600000", + "notionalFloor": "300000", + "maintMarginRatio": "0.1", + "cum": "15875.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 600000.0, + "maxNotional": 750000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "73175.0" + "initialLeverage": "4", + "notionalCap": "750000", + "notionalFloor": "600000", + "maintMarginRatio": "0.125", + "cum": "30875.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 750000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "1500000", + "notionalFloor": "750000", + "maintMarginRatio": "0.25", + "cum": "124625.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "3000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.5", - "cum": "323175.0" + "cum": "499625.0" } } ], diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7f7fccca8..428083f5f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -8,7 +8,7 @@ import logging import signal from copy import deepcopy from datetime import datetime, timedelta, timezone -from math import floor +from math import floor, isnan from threading import Lock from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union @@ -24,19 +24,21 @@ from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHAN 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, PriceType, RunMode, TradingMode -from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, - InvalidOrderException, OperationalException, PricingError, - RetryableOrderError, TemporaryError) +from freqtrade.exceptions import (ConfigurationError, DDosProtection, ExchangeError, + InsufficientFundsError, InvalidOrderException, + OperationalException, PricingError, RetryableOrderError, + TemporaryError) 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) + market_is_active, price_to_precision) +from freqtrade.exchange.exchange_utils_timeframe import (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) @@ -87,6 +89,8 @@ class Exchange: "order_props_in_contracts": ['amount', 'filled', 'remaining'], # Override createMarketBuyOrderRequiresPrice where ccxt has it wrong "marketOrderRequiresPrice": False, + "exchange_has_overrides": {}, # Dictionary overriding ccxt's "has". + # Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False} } _ft_has: Dict = {} _ft_has_futures: Dict = {} @@ -526,7 +530,7 @@ class Exchange: ) quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: - raise OperationalException( + raise ConfigurationError( f"{stake_currency} is not available as stake on {self.name}. " f"Available currencies are: {', '.join(quote_currencies)}") @@ -594,7 +598,7 @@ class Exchange: f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}") if timeframe and (timeframe not in self.timeframes): - raise OperationalException( + raise ConfigurationError( f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}") if ( @@ -602,7 +606,7 @@ class Exchange: and self._config['runmode'] != RunMode.UTIL_EXCHANGE and timeframe_to_minutes(timeframe) < 1 ): - raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.") + raise ConfigurationError("Timeframes < 1m are currently not supported by Freqtrade.") def validate_ordertypes(self, order_types: Dict) -> None: """ @@ -610,7 +614,7 @@ class Exchange: """ if any(v == 'market' for k, v in order_types.items()): if not self.exchange_has('createMarketOrder'): - raise OperationalException( + raise ConfigurationError( f'Exchange {self.name} does not support market orders.') self.validate_stop_ordertypes(order_types) @@ -620,7 +624,7 @@ class Exchange: """ if (order_types.get("stoploss_on_exchange") and not self._ft_has.get("stoploss_on_exchange", False)): - raise OperationalException( + raise ConfigurationError( f'On exchange stoploss is not supported for {self.name}.' ) if self.trading_mode == TradingMode.FUTURES: @@ -630,17 +634,17 @@ class Exchange: and 'stoploss_price_type' in order_types and order_types['stoploss_price_type'] not in price_mapping ): - raise OperationalException( + raise ConfigurationError( 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'): - raise OperationalException(f'Orderbook not available for {self.name}.') + raise ConfigurationError(f'Orderbook not available for {self.name}.') if (not pricing.get('use_order_book', False) and ( not self.exchange_has('fetchTicker') or not self._ft_has['tickers_have_price'])): - raise OperationalException(f'Ticker pricing not available for {self.name}.') + raise ConfigurationError(f'Ticker pricing not available for {self.name}.') def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: """ @@ -648,7 +652,7 @@ class Exchange: """ if any(v.upper() not in self._ft_has["order_time_in_force"] for k, v in order_time_in_force.items()): - raise OperationalException( + raise ConfigurationError( f'Time in force policies are not supported for {self.name} yet.') def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int: @@ -659,7 +663,7 @@ class Exchange: candle_limit = self.ohlcv_candle_limit( timeframe, self._config['candle_type_def'], - int(date_minus_candles(timeframe, startup_candles).timestamp() * 1000) + dt_ts(date_minus_candles(timeframe, startup_candles)) if timeframe else None) # Require one more candle - to account for the still open candle. candle_count = startup_candles + 1 @@ -670,12 +674,12 @@ class Exchange: if required_candle_call_count > 5: # Only allow 5 calls per pair to somewhat limit the impact - raise OperationalException( + raise ConfigurationError( f"This strategy requires {startup_candles} candles to start, " "which is more than 5x " f"the amount of candles {self.name} provides for {timeframe}.") elif required_candle_call_count > 1: - raise OperationalException( + raise ConfigurationError( f"This strategy requires {startup_candles} candles to start, which is more than " f"the amount of candles {self.name} provides for {timeframe}.") if required_candle_call_count > 1: @@ -716,6 +720,8 @@ class Exchange: :param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers') :return: bool """ + if endpoint in self._ft_has.get('exchange_has_overrides', {}): + return self._ft_has['exchange_has_overrides'][endpoint] return endpoint in self._api.has and self._api.has[endpoint] def get_precision_amount(self, pair: str) -> Optional[float]: @@ -2043,7 +2049,7 @@ class Exchange: timeframe, candle_type, since_ms) move_to = one_call * self.required_candle_call_count now = timeframe_to_next_date(timeframe) - since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000) + since_ms = dt_ts(now - timedelta(seconds=move_to // 1000)) if since_ms: return self._async_get_historic_ohlcv( @@ -2503,7 +2509,7 @@ class Exchange: ) if type(since) is datetime: - since = int(since.timestamp()) * 1000 # * 1000 for ms + since = dt_ts(since) try: funding_history = self._api.fetch_funding_history( @@ -2833,7 +2839,7 @@ class Exchange: if not close_date: close_date = datetime.now(timezone.utc) - since_ms = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000 + since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date)) mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type) funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE) @@ -2887,7 +2893,7 @@ class Exchange: else: # Fill up missing funding_rate candles with fallback value combined = mark_rates.merge( - funding_rates, on='date', how="outer", suffixes=["_mark", "_fund"] + funding_rates, on='date', how="left", suffixes=["_mark", "_fund"] ) combined['open_fund'] = combined['open_fund'].fillna(futures_funding_rate) return combined @@ -2916,7 +2922,8 @@ class Exchange: if not df.empty: df1 = df[(df['date'] >= open_date) & (df['date'] <= close_date)] fees = sum(df1['open_fund'] * df1['open_mark'] * amount) - + if isnan(fees): + fees = 0.0 # Negate fees for longs as funding_fees expects it this way based on live endpoints. return fees if is_short else -fees @@ -3092,3 +3099,4 @@ class Exchange: # describes the min amt for a tier, and the lowest tier will always go down to 0 else: raise ExchangeError(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 f4dc3a721..73f61f256 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -11,9 +11,9 @@ from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGIT from freqtrade.exchange.common import (BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, SUPPORTED_EXCHANGES) +from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.types import ValidExchangesType from freqtrade.util import FtPrecise -from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts CcxtModuleType = Any @@ -108,78 +108,6 @@ def list_available_exchanges(all_exchanges: bool) -> List[ValidExchangesType]: return exchanges_valid -def timeframe_to_seconds(timeframe: str) -> int: - """ - Translates the timeframe interval value written in the human readable - form ('1m', '5m', '1h', '1d', '1w', etc.) to the number - of seconds for one timeframe interval. - """ - return ccxt.Exchange.parse_timeframe(timeframe) - - -def timeframe_to_minutes(timeframe: str) -> int: - """ - Same as timeframe_to_seconds, but returns minutes. - """ - return ccxt.Exchange.parse_timeframe(timeframe) // 60 - - -def timeframe_to_msecs(timeframe: str) -> int: - """ - Same as timeframe_to_seconds, but returns milliseconds. - """ - return ccxt.Exchange.parse_timeframe(timeframe) * 1000 - - -def timeframe_to_resample_freq(timeframe: str) -> str: - """ - Translates the timeframe interval value written in the human readable - form ('1m', '5m', '1h', '1d', '1w', etc.) to the resample frequency - used by pandas ('1T', '5T', '1H', '1D', '1W', etc.) - """ - if timeframe == '1y': - return '1YS' - timeframe_seconds = timeframe_to_seconds(timeframe) - timeframe_minutes = timeframe_seconds // 60 - resample_interval = f'{timeframe_seconds}s' - if 10000 < timeframe_minutes < 43200: - resample_interval = '1W-MON' - elif timeframe_minutes >= 43200 and timeframe_minutes < 525600: - # Monthly candles need special treatment to stick to the 1st of the month - resample_interval = f'{timeframe}S' - elif timeframe_minutes > 43200: - resample_interval = timeframe - return resample_interval - - -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. - :param timeframe: timeframe in string format (e.g. "5m") - :param date: date to use. Defaults to now(utc) - :returns: date of previous candle (with utc timezone) - """ - if not date: - date = datetime.now(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: Optional[datetime] = None) -> datetime: - """ - Use Timeframe and determine next candle. - :param timeframe: timeframe in string format (e.g. "5m") - :param date: date to use. Defaults to now(utc) - :returns: date of next candle (with utc timezone) - """ - if not date: - date = datetime.now(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( timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime: """ diff --git a/freqtrade/exchange/exchange_utils_timeframe.py b/freqtrade/exchange/exchange_utils_timeframe.py new file mode 100644 index 000000000..9366bc7a1 --- /dev/null +++ b/freqtrade/exchange/exchange_utils_timeframe.py @@ -0,0 +1,81 @@ +from datetime import datetime, timezone +from typing import Optional + +import ccxt +from ccxt import ROUND_DOWN, ROUND_UP + +from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts + + +def timeframe_to_seconds(timeframe: str) -> int: + """ + Translates the timeframe interval value written in the human readable + form ('1m', '5m', '1h', '1d', '1w', etc.) to the number + of seconds for one timeframe interval. + """ + return ccxt.Exchange.parse_timeframe(timeframe) + + +def timeframe_to_minutes(timeframe: str) -> int: + """ + Same as timeframe_to_seconds, but returns minutes. + """ + return ccxt.Exchange.parse_timeframe(timeframe) // 60 + + +def timeframe_to_msecs(timeframe: str) -> int: + """ + Same as timeframe_to_seconds, but returns milliseconds. + """ + return ccxt.Exchange.parse_timeframe(timeframe) * 1000 + + +def timeframe_to_resample_freq(timeframe: str) -> str: + """ + Translates the timeframe interval value written in the human readable + form ('1m', '5m', '1h', '1d', '1w', etc.) to the resample frequency + used by pandas ('1T', '5T', '1H', '1D', '1W', etc.) + """ + if timeframe == '1y': + return '1YS' + timeframe_seconds = timeframe_to_seconds(timeframe) + timeframe_minutes = timeframe_seconds // 60 + resample_interval = f'{timeframe_seconds}s' + if 10000 < timeframe_minutes < 43200: + resample_interval = '1W-MON' + elif timeframe_minutes >= 43200 and timeframe_minutes < 525600: + # Monthly candles need special treatment to stick to the 1st of the month + resample_interval = f'{timeframe}S' + elif timeframe_minutes > 43200: + resample_interval = timeframe + return resample_interval + + +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. + :param timeframe: timeframe in string format (e.g. "5m") + :param date: date to use. Defaults to now(utc) + :returns: date of previous candle (with utc timezone) + """ + if not date: + date = datetime.now(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: Optional[datetime] = None) -> datetime: + """ + Use Timeframe and determine next candle. + :param timeframe: timeframe in string format (e.g. "5m") + :param date: date to use. Defaults to now(utc) + :returns: date of next candle (with utc timezone) + """ + if not date: + date = datetime.now(timezone.utc) + new_timestamp = ccxt.Exchange.round_timeframe( + timeframe, dt_ts(date), ROUND_UP) // 1000 + return dt_from_ts(new_timestamp) diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index ffef21402..cb749cb66 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -96,9 +96,7 @@ class Gate(Exchange): return trades def get_order_id_conditional(self, order: Dict[str, Any]) -> str: - if self.trading_mode == TradingMode.FUTURES: - return safe_value_fallback2(order, order, 'id_stop', 'id') - return order['id'] + return safe_value_fallback2(order, order, 'id_stop', 'id') def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: order = self.fetch_order( @@ -106,17 +104,19 @@ class Gate(Exchange): pair=pair, params={'stop': True} ) - if self.trading_mode == TradingMode.FUTURES: - if order['status'] == 'closed': - # Places a real order - which we need to fetch explicitly. - new_orderid = order.get('info', {}).get('trade_id') - if new_orderid: - order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params) - order1['id_stop'] = order1['id'] - order1['id'] = order_id - order1['stopPrice'] = order.get('stopPrice') + if order.get('status', 'open') == 'closed': + # Places a real order - which we need to fetch explicitly. + val = 'trade_id' if self.trading_mode == TradingMode.FUTURES else 'fired_order_id' - return order1 + if new_orderid := order.get('info', {}).get(val): + order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params) + order1['id_stop'] = order1['id'] + order1['id'] = order_id + order1['type'] = 'stoploss' + order1['stopPrice'] = order.get('stopPrice') + order1['status_stop'] = 'triggered' + + return order1 return order def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 574bec348..d9bc8d3a4 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -825,7 +825,7 @@ class IFreqaiModel(ABC): """ if self.config.get("freqai_backtest_live_models", False) and len_dataframe_backtest == 0: logger.info(f"No data found for pair {pair} from " - f"from { tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. " + f"from {tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. " "Probably more than one training within the same candle period.") return False return True diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index f1a2474da..f3de6653b 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -36,8 +36,15 @@ class XGBoostRegressor(BaseRegressionModel): eval_set = None eval_weights = None else: - eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] - eval_weights = [data_dictionary['test_weights']] + eval_set = [ + (data_dictionary["test_features"], + data_dictionary["test_labels"]), + (X, y) + ] + eval_weights = [ + data_dictionary['test_weights'], + data_dictionary['train_weights'] + ] sample_weight = data_dictionary["train_weights"] diff --git a/freqtrade/freqai/tensorboard/tensorboard.py b/freqtrade/freqai/tensorboard/tensorboard.py index 46bf8dc61..3ad896108 100644 --- a/freqtrade/freqai/tensorboard/tensorboard.py +++ b/freqtrade/freqai/tensorboard/tensorboard.py @@ -43,13 +43,11 @@ class TensorBoardCallback(BaseTensorBoardCallback): if not evals_log: return False - for data, metric in evals_log.items(): - for metric_name, log in metric.items(): + evals = ["validation", "train"] + for metric, eval in zip(evals_log.items(), evals): + for metric_name, log in metric[1].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) + self.writer.add_scalar(f"{eval}-{metric_name}", score, epoch) return False diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index 371a953e7..a91513dcb 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -152,7 +152,7 @@ class PyTorchModelTrainer(PyTorchTrainerInterface): """ assert isinstance(self.n_steps, int), "Either `n_steps` or `n_epochs` should be set." n_batches = n_obs // self.batch_size - n_epochs = min(self.n_steps // n_batches, 1) + n_epochs = max(self.n_steps // n_batches, 1) if n_epochs <= 10: logger.warning( f"Setting low n_epochs: {n_epochs}. " diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 974f8124e..0184741ff 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -37,7 +37,6 @@ from freqtrade.rpc.rpc_types import (ProfitLossStr, RPCCancelMsg, RPCEntryMsg, R RPCExitMsg, RPCProtectionMsg) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from freqtrade.util import FtPrecise from freqtrade.util.migrations import migrate_binance_futures_names from freqtrade.wallets import Wallets @@ -667,7 +666,7 @@ class FreqtradeBot(LoggingMixin): # We should decrease our position amount = self.exchange.amount_to_contract_precision( trade.pair, - abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate)))) + abs(float(stake_amount * trade.amount / trade.stake_amount))) if amount == 0.0: logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.") @@ -962,7 +961,7 @@ class FreqtradeBot(LoggingMixin): # edge-case for now. min_stake_amount = self.exchange.get_min_pair_stake_amount( pair, enter_limit_requested, - self.strategy.stoploss if not mode != 'pos_adjust' else 0.0, + self.strategy.stoploss if not mode == 'pos_adjust' else 0.0, leverage) max_stake_amount = self.exchange.get_max_pair_stake_amount( pair, enter_limit_requested, leverage) @@ -1945,6 +1944,9 @@ class FreqtradeBot(LoggingMixin): def _update_trade_after_fill(self, trade: Trade, order: Order) -> Trade: if order.status in constants.NON_OPEN_EXCHANGE_STATES: + strategy_safe_wrapper( + self.strategy.order_filled, default_retval=None)( + pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc)) # If a entry order was closed, force update on stoploss on exchange if order.ft_order_side == trade.entry_side: trade = self.cancel_stoploss_on_exchange(trade) diff --git a/freqtrade/main.py b/freqtrade/main.py index 05e5409ad..5eabe398f 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -7,8 +7,6 @@ import logging import sys from typing import Any, List, Optional -from freqtrade.util.gc_setup import gc_set_threshold - # check min. python version if sys.version_info < (3, 9): # pragma: no cover @@ -16,8 +14,10 @@ if sys.version_info < (3, 9): # pragma: no cover from freqtrade import __version__ from freqtrade.commands import Arguments -from freqtrade.exceptions import FreqtradeException, OperationalException +from freqtrade.constants import DOCS_LINK +from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException from freqtrade.loggers import setup_logging_pre +from freqtrade.util.gc_setup import gc_set_threshold logger = logging.getLogger('freqtrade') @@ -56,6 +56,9 @@ def main(sysargv: Optional[List[str]] = None) -> None: except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') return_code = 0 + except ConfigurationError as e: + logger.error(f"Configuration error: {e}\n" + f"Please make sure to review the documentation at {DOCS_LINK}.") except FreqtradeException as e: logger.error(str(e)) return_code = 2 diff --git a/freqtrade/optimize/analysis/lookahead_helpers.py b/freqtrade/optimize/analysis/lookahead_helpers.py index 1d2b9db48..d2cc541f2 100644 --- a/freqtrade/optimize/analysis/lookahead_helpers.py +++ b/freqtrade/optimize/analysis/lookahead_helpers.py @@ -107,9 +107,9 @@ class LookaheadAnalysisSubFunctions: csv_df = add_or_update_row(csv_df, new_row_data) # Fill NaN values with a default value (e.g., 0) - csv_df['total_signals'] = csv_df['total_signals'].fillna(0) - csv_df['biased_entry_signals'] = csv_df['biased_entry_signals'].fillna(0) - csv_df['biased_exit_signals'] = csv_df['biased_exit_signals'].fillna(0) + csv_df['total_signals'] = csv_df['total_signals'].astype(int).fillna(0) + csv_df['biased_entry_signals'] = csv_df['biased_entry_signals'].astype(int).fillna(0) + csv_df['biased_exit_signals'] = csv_df['biased_exit_signals'].astype(int).fillna(0) # Convert columns to integers csv_df['total_signals'] = csv_df['total_signals'].astype(int) @@ -121,14 +121,22 @@ class LookaheadAnalysisSubFunctions: @staticmethod def calculate_config_overrides(config: Config): + if config.get('enable_protections', False): + # if protections are used globally, they can produce false positives. + config['enable_protections'] = False + logger.info('Protections were enabled. ' + 'Disabling protections now ' + 'since they could otherwise produce false positives.') if config['targeted_trade_amount'] < config['minimum_trade_amount']: # this combo doesn't make any sense. raise OperationalException( "Targeted trade amount can't be smaller than minimum trade amount." ) - if len(config['pairs']) > config['max_open_trades']: - logger.info('Max_open_trades were less than amount of pairs. ' - 'Set max_open_trades to amount of pairs just to avoid false positives.') + if len(config['pairs']) > config.get('max_open_trades', 0): + logger.info('Max_open_trades were less than amount of pairs ' + 'or defined in the strategy. ' + 'Set max_open_trades to amount of pairs ' + 'just to avoid false positives.') config['max_open_trades'] = len(config['pairs']) min_dry_run_wallet = 1000000000 diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8d16122ea..326c48c62 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -33,8 +33,8 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera show_backtest_results, store_backtest_analysis_results, store_backtest_stats) -from freqtrade.persistence import (LocalTrade, Order, PairLocks, Trade, disable_database_use, - enable_database_use) +from freqtrade.persistence import (CustomDataWrapper, LocalTrade, Order, PairLocks, Trade, + disable_database_use, enable_database_use) from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -337,6 +337,7 @@ class Backtesting: self.disable_database_use() PairLocks.reset_locks() Trade.reset_trades() + CustomDataWrapper.reset_custom_data() self.rejected_trades = 0 self.timedout_entry_orders = 0 self.timedout_exit_orders = 0 @@ -602,6 +603,11 @@ class Backtesting: if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_date, trade) self._run_funding_fees(trade, current_date, force=True) + strategy_safe_wrapper( + self.strategy.order_filled, + default_retval=None)( + pair=trade.pair, trade=trade, # type: ignore[arg-type] + order=order, current_time=current_date) if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): # trade is still open @@ -881,6 +887,9 @@ class Backtesting: precision_amount = self.exchange.get_precision_amount(pair) amount = amount_to_contract_precision(amount_p, precision_amount, self.precision_mode, contract_size) + if not amount: + # No amount left after truncating to precision. + return trade # Backcalculate actual stake amount. stake_amount = amount * propose_rate / leverage diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 059207a88..2a4be7e7a 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -16,7 +16,7 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) """ - return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f', + return ['s', 'd', '.2f', f'.{decimals_per_coin(stake_currency)}f', '.2f', 'd', 's', 's'] @@ -25,7 +25,7 @@ def _get_line_header(first_column: str, stake_currency: str, """ Generate header lines (goes in line with _generate_result_line()) """ - return [first_column, direction, 'Avg Profit %', 'Cum Profit %', + return [first_column, direction, 'Avg Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', 'Win Draw Loss Win%'] @@ -51,7 +51,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st headers = _get_line_header('Pair', stake_currency) floatfmt = _get_line_floatfmt(stake_currency) output = [[ - t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['key'], t['trades'], t['profit_mean_pct'], t['profit_total_abs'], t['profit_total_pct'], t['duration_avg'], generate_wins_draws_losses(t['wins'], t['draws'], t['losses']) ] for t in pair_results] @@ -72,7 +72,6 @@ def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_curren 'Exits', 'Win Draws Loss Win%', 'Avg Profit %', - 'Cum Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', ] @@ -80,7 +79,7 @@ def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_curren output = [[ t.get('exit_reason', t.get('sell_reason')), t['trades'], generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), - t['profit_mean_pct'], t['profit_sum_pct'], + t['profit_mean_pct'], fmt_coin(t['profit_total_abs'], stake_currency, False), t['profit_total_pct'], ] for t in exit_reason_stats] @@ -105,7 +104,6 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr t['key']) > 0 else "OTHER", t['trades'], t['profit_mean_pct'], - t['profit_sum_pct'], t['profit_total_abs'], t['profit_total_pct'], t['duration_avg'], @@ -166,7 +164,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: for t, dd in zip(strategy_results, drawdown)] output = [[ - t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], + t['key'], t['trades'], t['profit_mean_pct'], t['profit_total_abs'], t['profit_total_pct'], t['duration_avg'], generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), drawdown] for t, drawdown in zip(strategy_results, drawdown)] @@ -256,9 +254,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: *short_metrics, ('', ''), # Empty line to improve readability ('Best Pair', f"{strat_results['best_pair']['key']} " - f"{strat_results['best_pair']['profit_sum']:.2%}"), + f"{strat_results['best_pair']['profit_total']:.2%}"), ('Worst Pair', f"{strat_results['worst_pair']['key']} " - f"{strat_results['worst_pair']['profit_sum']:.2%}"), + f"{strat_results['worst_pair']['profit_total']:.2%}"), ('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"), ('Worst trade', f"{worst_trade['pair']} " f"{worst_trade['profit_ratio']:.2%}"), diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 47a13dcd8..47aab2a62 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -215,7 +215,7 @@ def _get_resample_from_period(period: str) -> str: # Weekly defaulting to Monday. return '1W-MON' if period == 'month': - return '1M' + return '1ME' raise ValueError(f"Period {period} is not supported.") diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index 220502e69..61aad0597 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -20,8 +20,10 @@ class SKDecimal(Integer): super().__init__(_low, _high, prior, base, transform, name, dtype) def __repr__(self): - return "Decimal(low={}, high={}, decimals={}, prior='{}', transform='{}')".format( - self.low_orig, self.high_orig, self.decimals, self.prior, self.transform_) + return ( + f"Decimal(low={self.low_orig}, high={self.high_orig}, decimals={self.decimals}, " + f"prior='{self.prior}', transform='{self.transform_}')" + ) def __contains__(self, point): if isinstance(point, list): diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 6205174a7..d5584c22c 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.persistence.custom_data import CustomDataWrapper from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py new file mode 100644 index 000000000..81a9e7ad6 --- /dev/null +++ b/freqtrade/persistence/custom_data.py @@ -0,0 +1,174 @@ +import json +import logging +from datetime import datetime +from typing import Any, ClassVar, List, Optional, Sequence + +from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, select +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.persistence.base import ModelBase, SessionType +from freqtrade.util import dt_now + + +logger = logging.getLogger(__name__) + + +class _CustomData(ModelBase): + """ + CustomData database model + Keeps records of metadata as key/value store + for trades or global persistant values + One to many relationship with Trades: + - One trade can have many metadata entries + - One metadata entry can only be associated with one Trade + """ + __tablename__ = 'trade_custom_data' + __allow_unmapped__ = True + 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_trade_id', 'cd_key', name="_trade_id_cd_key"),) + + id = mapped_column(Integer, primary_key=True) + ft_trade_id = mapped_column(Integer, ForeignKey('trades.id'), index=True) + + trade = relationship("Trade", back_populates="custom_data") + + cd_key: Mapped[str] = mapped_column(String(255), nullable=False) + cd_type: Mapped[str] = mapped_column(String(25), nullable=False) + cd_value: Mapped[str] = mapped_column(Text, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=dt_now) + updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + + # Empty container value - not persisted, but filled with cd_value on query + value: Any = None + + def __repr__(self): + create_time = (self.created_at.strftime(DATETIME_PRINT_FORMAT) + if self.created_at is not None else None) + update_time = (self.updated_at.strftime(DATETIME_PRINT_FORMAT) + if self.updated_at is not None else None) + return (f'CustomData(id={self.id}, key={self.cd_key}, type={self.cd_type}, ' + + f'value={self.cd_value}, trade_id={self.ft_trade_id}, created={create_time}, ' + + f'updated={update_time})') + + @classmethod + def query_cd(cls, key: Optional[str] = None, + trade_id: Optional[int] = None) -> Sequence['_CustomData']: + """ + Get all CustomData, if trade_id is not specified + return will be for generic values not tied to a trade + :param trade_id: id of the Trade + """ + filters = [] + if trade_id is not None: + filters.append(_CustomData.ft_trade_id == trade_id) + if key is not None: + filters.append(_CustomData.cd_key.ilike(key)) + + return _CustomData.session.scalars(select(_CustomData).filter(*filters)).all() + + +class CustomDataWrapper: + """ + CustomData middleware class + Abstracts the database layer away so it becomes optional - which will be necessary to support + backtesting and hyperopt in the future. + """ + + use_db = True + custom_data: List[_CustomData] = [] + unserialized_types = ['bool', 'float', 'int', 'str'] + + @staticmethod + def _convert_custom_data(data: _CustomData) -> _CustomData: + if data.cd_type in CustomDataWrapper.unserialized_types: + data.value = data.cd_value + if data.cd_type == 'bool': + data.value = data.cd_value.lower() == 'true' + elif data.cd_type == 'int': + data.value = int(data.cd_value) + elif data.cd_type == 'float': + data.value = float(data.cd_value) + else: + data.value = json.loads(data.cd_value) + return data + + @staticmethod + def reset_custom_data() -> None: + """ + Resets all key-value pairs. Only active for backtesting mode. + """ + if not CustomDataWrapper.use_db: + CustomDataWrapper.custom_data = [] + + @staticmethod + def delete_custom_data(trade_id: int) -> None: + _CustomData.session.query(_CustomData).filter(_CustomData.ft_trade_id == trade_id).delete() + _CustomData.session.commit() + + @staticmethod + def get_custom_data(*, trade_id: int, key: Optional[str] = None) -> List[_CustomData]: + + if CustomDataWrapper.use_db: + filters = [ + _CustomData.ft_trade_id == trade_id, + ] + if key is not None: + filters.append(_CustomData.cd_key.ilike(key)) + filtered_custom_data = _CustomData.session.scalars(select(_CustomData).filter( + *filters)).all() + + else: + filtered_custom_data = [ + data_entry for data_entry in CustomDataWrapper.custom_data + if (data_entry.ft_trade_id == trade_id) + ] + if key is not None: + filtered_custom_data = [ + data_entry for data_entry in filtered_custom_data + if (data_entry.cd_key.casefold() == key.casefold()) + ] + return [CustomDataWrapper._convert_custom_data(d) for d in filtered_custom_data] + + @staticmethod + def set_custom_data(trade_id: int, key: str, value: Any) -> None: + + value_type = type(value).__name__ + + if value_type not in CustomDataWrapper.unserialized_types: + try: + value_db = json.dumps(value) + except TypeError as e: + logger.warning(f"could not serialize {key} value due to {e}") + return + else: + value_db = str(value) + + if trade_id is None: + trade_id = 0 + + custom_data = CustomDataWrapper.get_custom_data(trade_id=trade_id, key=key) + if custom_data: + data_entry = custom_data[0] + data_entry.cd_value = value_db + data_entry.updated_at = dt_now() + else: + data_entry = _CustomData( + ft_trade_id=trade_id, + cd_key=key, + cd_type=value_type, + cd_value=value_db, + created_at=dt_now(), + ) + data_entry.value = value + + if CustomDataWrapper.use_db and value_db is not None: + _CustomData.session.add(data_entry) + _CustomData.session.commit() + else: + if not custom_data: + CustomDataWrapper.custom_data.append(data_entry) + # Existing data will have updated interactively. diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e561e727b..1a69b271c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -13,6 +13,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.persistence.base import ModelBase +from freqtrade.persistence.custom_data import _CustomData from freqtrade.persistence.key_value_store import _KeyValueStoreModel from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock @@ -78,6 +79,8 @@ def init_db(db_url: str) -> None: Order.session = Trade.session PairLock.session = Trade.session _KeyValueStoreModel.session = Trade.session + _CustomData.session = scoped_session(sessionmaker(bind=engine, autoflush=True), + scopefunc=get_request_or_thread_id) previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 407affe72..defeb0e3f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -23,6 +23,7 @@ from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precisi from freqtrade.leverage import interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.base import ModelBase, SessionType +from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none @@ -1214,6 +1215,40 @@ class LocalTrade: or (o.ft_is_open is True and o.status is not None) ] + def set_custom_data(self, key: str, value: Any) -> None: + """ + Set custom data for this trade + :param key: key of the custom data + :param value: value of the custom data (must be JSON serializable) + """ + CustomDataWrapper.set_custom_data(trade_id=self.id, key=key, value=value) + + def get_custom_data(self, key: str, default: Any = None) -> Any: + """ + Get custom data for this trade + :param key: key of the custom data + """ + data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key) + if data: + return data[0].value + return default + + def get_custom_data_entry(self, key: str) -> Optional[_CustomData]: + """ + Get custom data for this trade + :param key: key of the custom data + """ + data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key) + if data: + return data[0] + return None + + def get_all_custom_data(self) -> List[_CustomData]: + """ + Get all custom data for this trade + """ + return CustomDataWrapper.get_custom_data(trade_id=self.id) + @property def nr_of_successful_entries(self) -> int: """ @@ -1469,6 +1504,9 @@ class Trade(ModelBase, LocalTrade): orders: Mapped[List[Order]] = relationship( "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", innerjoin=True) # type: ignore + custom_data: Mapped[List[_CustomData]] = relationship( + "_CustomData", cascade="all, delete-orphan", + lazy="raise") exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore @@ -1572,6 +1610,8 @@ class Trade(ModelBase, LocalTrade): for order in self.orders: Order.session.delete(order) + CustomDataWrapper.delete_custom_data(trade_id=self.id) + Trade.session.delete(self) Trade.commit() diff --git a/freqtrade/persistence/usedb_context.py b/freqtrade/persistence/usedb_context.py index 6fffd2fb5..732f0b0f8 100644 --- a/freqtrade/persistence/usedb_context.py +++ b/freqtrade/persistence/usedb_context.py @@ -1,4 +1,5 @@ +from freqtrade.persistence.custom_data import CustomDataWrapper from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import Trade @@ -11,6 +12,7 @@ def disable_database_use(timeframe: str) -> None: PairLocks.use_db = False PairLocks.timeframe = timeframe Trade.use_db = False + CustomDataWrapper.use_db = False def enable_database_use() -> None: @@ -20,6 +22,7 @@ def enable_database_use() -> None: PairLocks.use_db = True PairLocks.timeframe = '' Trade.use_db = True + CustomDataWrapper.use_db = True class FtNoDBContext: diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index ef72486e1..cdd171e91 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -3,7 +3,6 @@ Volatility pairlist filter """ import logging import sys -from copy import deepcopy from datetime import timedelta from typing import Any, Dict, List, Optional @@ -37,6 +36,7 @@ class VolatilityFilter(IPairList): self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) self._refresh_period = pairlistconfig.get('refresh_period', 1440) self._def_candletype = self._config['candle_type_def'] + self._sort_direction: Optional[str] = pairlistconfig.get('sort_direction', None) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) @@ -46,6 +46,9 @@ class VolatilityFilter(IPairList): if self._days > candle_limit: raise OperationalException("VolatilityFilter requires lookback_days to not " f"exceed exchange max request size ({candle_limit})") + if self._sort_direction not in [None, 'asc', 'desc']: + raise OperationalException("VolatilityFilter requires sort_direction to be " + "either None (undefined), 'asc' or 'desc'") @property def needstickers(self) -> bool: @@ -89,6 +92,13 @@ class VolatilityFilter(IPairList): "description": "Maximum Volatility", "help": "Maximum volatility a pair must have to be considered.", }, + "sort_direction": { + "type": "option", + "default": None, + "options": ["", "asc", "desc"], + "description": "Sort pairlist", + "help": "Sort Pairlist ascending or descending by volatility.", + }, **IPairList.refresh_period_parameter() } @@ -105,43 +115,61 @@ class VolatilityFilter(IPairList): since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days)) candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) - if self._enabled: - for p in deepcopy(pairlist): - daily_candles = candles[(p, '1d', self._def_candletype)] if ( - p, '1d', self._def_candletype) in candles else None - if not self._validate_pair_loc(p, daily_candles): - pairlist.remove(p) - return pairlist + resulting_pairlist: List[str] = [] + volatilitys: Dict[str, float] = {} + for p in pairlist: + daily_candles = candles.get((p, '1d', self._def_candletype), None) - def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: - """ - Validate trading range - :param pair: Pair that's currently validated - :param daily_candles: Downloaded daily candles - :return: True if the pair can stay, false if it should be removed - """ + volatility_avg = self._calculate_volatility(p, daily_candles) + + if volatility_avg is not None: + if self._validate_pair_loc(p, volatility_avg): + resulting_pairlist.append(p) + volatilitys[p] = ( + volatility_avg if volatility_avg and not np.isnan(volatility_avg) else 0 + ) + else: + self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) + + if self._sort_direction: + resulting_pairlist = sorted(resulting_pairlist, + key=lambda p: volatilitys[p], + reverse=self._sort_direction == 'desc') + return resulting_pairlist + + def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> Optional[float]: # Check symbol in cache - if (cached_res := self._pair_cache.get(pair, None)) is not None: - return cached_res + if (volatility_avg := self._pair_cache.get(pair, None)) is not None: + return volatility_avg - result = False if daily_candles is not None and not daily_candles.empty: returns = (np.log(daily_candles["close"].shift(1) / daily_candles["close"])) returns.fillna(0, inplace=True) volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days) volatility_avg = volatility_series.mean() + self._pair_cache[pair] = volatility_avg - if self._min_volatility <= volatility_avg <= self._max_volatility: - result = True - else: - self.log_once(f"Removed {pair} from whitelist, because volatility " - f"over {self._days} {plural(self._days, 'day')} " - f"is: {volatility_avg:.3f} " - f"which is not in the configured range of " - f"{self._min_volatility}-{self._max_volatility}.", - logger.info) - result = False - self._pair_cache[pair] = result + return volatility_avg + else: + return None + def _validate_pair_loc(self, pair: str, volatility_avg: float) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param volatility_avg: Average volatility + :return: True if the pair can stay, false if it should be removed + """ + + if self._min_volatility <= volatility_avg <= self._max_volatility: + result = True + else: + self.log_once(f"Removed {pair} from whitelist, because volatility " + f"over {self._days} {plural(self._days, 'day')} " + f"is: {volatility_avg:.3f} " + f"which is not in the configured range of " + f"{self._min_volatility}-{self._max_volatility}.", + logger.info) + result = False return result diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index f4d08e800..25e0a855d 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -41,6 +41,7 @@ class VolumePairList(IPairList): self._number_pairs = self._pairlistconfig['number_assets'] self._sort_key: Literal['quoteVolume'] = self._pairlistconfig.get('sort_key', 'quoteVolume') self._min_value = self._pairlistconfig.get('min_value', 0) + self._max_value = self._pairlistconfig.get("max_value", None) self._refresh_period = self._pairlistconfig.get('refresh_period', 1800) self._pair_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) self._lookback_days = self._pairlistconfig.get('lookback_days', 0) @@ -139,6 +140,12 @@ class VolumePairList(IPairList): "description": "Minimum value", "help": "Minimum value to use for filtering the pairlist.", }, + "max_value": { + "type": "number", + "default": None, + "description": "Maximum value", + "help": "Maximum value to use for filtering the pairlist.", + }, **IPairList.refresh_period_parameter(), "lookback_days": { "type": "number", @@ -270,6 +277,9 @@ class VolumePairList(IPairList): if self._min_value > 0: filtered_tickers = [ v for v in filtered_tickers if v[self._sort_key] > self._min_value] + if self._max_value is not None: + filtered_tickers = [ + v for v in filtered_tickers if v[self._sort_key] < self._max_value] sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[self._sort_key]) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 49fba59b9..0480f60d0 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -2,7 +2,6 @@ Rate of change pairlist filter """ import logging -from copy import deepcopy from datetime import timedelta from typing import Any, Dict, List, Optional @@ -32,6 +31,7 @@ class RangeStabilityFilter(IPairList): self._max_rate_of_change = pairlistconfig.get('max_rate_of_change') self._refresh_period = pairlistconfig.get('refresh_period', 86400) self._def_candletype = self._config['candle_type_def'] + self._sort_direction: Optional[str] = pairlistconfig.get('sort_direction', None) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) @@ -41,6 +41,9 @@ class RangeStabilityFilter(IPairList): if self._days > candle_limit: raise OperationalException("RangeStabilityFilter requires lookback_days to not " f"exceed exchange max request size ({candle_limit})") + if self._sort_direction not in [None, 'asc', 'desc']: + raise OperationalException("RangeStabilityFilter requires sort_direction to be " + "either None (undefined), 'asc' or 'desc'") @property def needstickers(self) -> bool: @@ -87,6 +90,13 @@ class RangeStabilityFilter(IPairList): "description": "Maximum Rate of Change", "help": "Maximum rate of change to filter pairs.", }, + "sort_direction": { + "type": "option", + "default": None, + "options": ["", "asc", "desc"], + "description": "Sort pairlist", + "help": "Sort Pairlist ascending or descending by rate of change.", + }, **IPairList.refresh_period_parameter() } @@ -103,45 +113,62 @@ class RangeStabilityFilter(IPairList): since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days + 1)) candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) - if self._enabled: - for p in deepcopy(pairlist): - daily_candles = candles[(p, '1d', self._def_candletype)] if ( - p, '1d', self._def_candletype) in candles else None - if not self._validate_pair_loc(p, daily_candles): - pairlist.remove(p) - return pairlist + resulting_pairlist: List[str] = [] + pct_changes: Dict[str, float] = {} - def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: - """ - Validate trading range - :param pair: Pair that's currently validated - :param daily_candles: Downloaded daily candles - :return: True if the pair can stay, false if it should be removed - """ + for p in pairlist: + daily_candles = candles.get((p, '1d', self._def_candletype), None) + + pct_change = self._calculate_rate_of_change(p, daily_candles) + + if pct_change is not None: + if self._validate_pair_loc(p, pct_change): + resulting_pairlist.append(p) + pct_changes[p] = pct_change + else: + self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) + + if self._sort_direction: + resulting_pairlist = sorted(resulting_pairlist, + key=lambda p: pct_changes[p], + reverse=self._sort_direction == 'desc') + return resulting_pairlist + + def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> Optional[float]: # Check symbol in cache - if (cached_res := self._pair_cache.get(pair, None)) is not None: - return cached_res - - result = True + if (pct_change := self._pair_cache.get(pair, None)) is not None: + return pct_change if daily_candles is not None and not daily_candles.empty: + highest_high = daily_candles['high'].max() lowest_low = daily_candles['low'].min() pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 - if pct_change < self._min_rate_of_change: - self.log_once(f"Removed {pair} from whitelist, because rate of change " - f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " - f"which is below the threshold of {self._min_rate_of_change}.", - logger.info) - result = False - if self._max_rate_of_change: - if pct_change > self._max_rate_of_change: - self.log_once( - f"Removed {pair} from whitelist, because rate of change " - f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " - f"which is above the threshold of {self._max_rate_of_change}.", - logger.info) - result = False - self._pair_cache[pair] = result + self._pair_cache[pair] = pct_change + return pct_change else: - self.log_once(f"Removed {pair} from whitelist, no candles found.", logger.info) + return None + + def _validate_pair_loc(self, pair: str, pct_change: float) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param pct_change: Rate of change + :return: True if the pair can stay, false if it should be removed + """ + + result = True + if pct_change < self._min_rate_of_change: + self.log_once(f"Removed {pair} from whitelist, because rate of change " + f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " + f"which is below the threshold of {self._min_rate_of_change}.", + logger.info) + result = False + if self._max_rate_of_change: + if pct_change > self._max_rate_of_change: + self.log_once( + f"Removed {pair} from whitelist, because rate of change " + f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " + f"which is above the threshold of {self._max_rate_of_change}.", + logger.info) + result = False return result diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index f387a9ac8..814dcf8da 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -14,7 +14,7 @@ from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_resu get_backtest_resultlist, load_and_merge_backtest_result, update_backtest_metadata) from freqtrade.enums import BacktestState -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException from freqtrade.exchange.common import remove_exchange_credentials from freqtrade.misc import deep_merge_dicts, is_file_in_dir from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMetadataUpdate, @@ -98,10 +98,12 @@ def __run_backtest_bg(btconfig: Config): logger.info("Backtest finished.") + except ConfigurationError as e: + logger.error(f"Backtesting encountered a configuration Error: {e}") + 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 diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 3ea9ed4d0..af8d8ddf4 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -559,3 +559,7 @@ class SysInfo(BaseModel): class Health(BaseModel): last_process: Optional[datetime] = None last_process_ts: Optional[int] = None + bot_start: Optional[datetime] = None + bot_start_ts: Optional[int] = None + bot_startup: Optional[datetime] = None + bot_startup_ts: Optional[int] = None diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 2453f4f25..d01596cfc 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -77,9 +77,8 @@ class CryptoToFiatConverter(LoggingMixin): return # If the request is not a 429 error we want to raise the normal error logger.error( - "Could not load FIAT Cryptocurrency map for the following problem: {}".format( - request_exception - ) + "Could not load FIAT Cryptocurrency map for the following problem: " + f"{request_exception}" ) except (Exception) as exception: logger.error( diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2317ee1a9..8bb7f754f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -291,6 +291,10 @@ class RPC: profit_str += f" ({fiat_profit:.2f})" fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ else fiat_profit_sum + fiat_profit + else: + profit_str += f" ({trade_profit:.2f})" + fiat_profit_sum = trade_profit if isnan(fiat_profit_sum) \ + else fiat_profit_sum + trade_profit active_attempt_side_symbols = [ '*' if (oo and oo.ft_order_side == trade.entry_side) else '**' @@ -317,6 +321,8 @@ class RPC: profitcol = "Profit" if self._fiat_converter: profitcol += " (" + fiat_display_currency + ")" + else: + profitcol += " (" + stake_currency + ")" columns = [ 'ID L/S' if nonspot else 'ID', @@ -927,6 +933,7 @@ class RPC: is_short=is_short, enter_tag=enter_tag, leverage_=leverage, + mode='pos_adjust' if trade else 'initial' ): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() @@ -999,6 +1006,32 @@ class RPC: 'cancel_order_count': c_count, } + def _rpc_list_custom_data(self, trade_id: int, key: Optional[str]) -> List[Dict[str, Any]]: + # Query for trade + trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() + if trade is None: + return [] + # Query custom_data + custom_data = [] + if key: + data = trade.get_custom_data(key=key) + if data: + custom_data = [data] + else: + custom_data = trade.get_all_custom_data() + return [ + { + 'id': data_entry.id, + 'ft_trade_id': data_entry.ft_trade_id, + 'cd_key': data_entry.cd_key, + 'cd_type': data_entry.cd_type, + 'cd_value': data_entry.cd_value, + 'created_at': data_entry.created_at, + 'updated_at': data_entry.updated_at + } + for data_entry in custom_data + ] + def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. @@ -1155,7 +1188,7 @@ class RPC: } if has_content: - dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000 + dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 # Move signal close to separate column when signal for easy plotting for sig_type in signals.keys(): if sig_type in dataframe.columns: @@ -1333,19 +1366,40 @@ class RPC: 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": format_date(last_p.astimezone(tzlocal())), - "last_process_ts": int(last_p.timestamp()), + res: Dict[str, Union[None, str, int]] = { + "last_process": None, + "last_process_loc": None, + "last_process_ts": None, + "bot_start": None, + "bot_start_loc": None, + "bot_start_ts": None, + "bot_startup": None, + "bot_startup_loc": None, + "bot_startup_ts": None, } + if last_p is not None: + res.update({ + "last_process": str(last_p), + "last_process_loc": format_date(last_p.astimezone(tzlocal())), + "last_process_ts": int(last_p.timestamp()), + }) + + if (bot_start := KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME)): + res.update({ + "bot_start": str(bot_start), + "bot_start_loc": format_date(bot_start.astimezone(tzlocal())), + "bot_start_ts": int(bot_start.timestamp()), + }) + if (bot_startup := KeyValueStore.get_datetime_value(KeyStoreKeys.STARTUP_TIME)): + res.update({ + "bot_startup": str(bot_startup), + "bot_startup_loc": format_date(bot_startup.astimezone(tzlocal())), + "bot_startup_ts": int(bot_startup.timestamp()), + }) + + return res + def _update_market_direction(self, direction: MarketDirection) -> None: self._freqtrade.strategy.market_direction = direction diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 904b1fdbc..b1da59549 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -33,7 +33,7 @@ from freqtrade.misc import chunks, plural from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException, RPCHandler from freqtrade.rpc.rpc_types import RPCEntryMsg, RPCExitMsg, RPCOrderMsg, RPCSendMsg -from freqtrade.util import dt_humanize, fmt_coin, round_value +from freqtrade.util import dt_humanize, fmt_coin, format_date, round_value MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH @@ -243,6 +243,7 @@ class Telegram(RPCHandler): CommandHandler('version', self._version), CommandHandler('marketdir', self._changemarketdir), CommandHandler('order', self._order), + CommandHandler('list_custom_data', self._list_custom_data), ] callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), @@ -355,9 +356,11 @@ class Telegram(RPCHandler): if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: message += f" ({msg['leverage']:.3g}x)" message += "`\n" - message += f"*Open Rate:* `{fmt_coin(msg['open_rate'], msg['quote_currency'])}`\n" + message += f"*Open Rate:* `{round_value(msg['open_rate'], 8)} {msg['quote_currency']}`\n" if msg['type'] == RPCMessageType.ENTRY and msg['current_rate']: - message += f"*Current Rate:* `{fmt_coin(msg['current_rate'], msg['quote_currency'])}`\n" + message += ( + f"*Current Rate:* `{round_value(msg['current_rate'], 8)} {msg['quote_currency']}`\n" + ) profit_fiat_extra = self.__format_profit_fiat(msg, 'stake_amount') # type: ignore total = fmt_coin(msg['stake_amount'], msg['quote_currency']) @@ -562,19 +565,19 @@ class Telegram(RPCHandler): lines.append(f"*{wording} #{order_nr}:*") if order_nr == 1: lines.append( - f"*Amount:* {cur_entry_amount:.8g} " + f"*Amount:* {round_value(cur_entry_amount, 8)} " f"({fmt_coin(order['cost'], quote_currency)})" ) - lines.append(f"*Average Price:* {cur_entry_average:.8g}") + lines.append(f"*Average Price:* {round_value(cur_entry_average, 8)}") else: # TODO: This calculation ignores fees. price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) if is_open: lines.append("({})".format(dt_humanize(order["order_filled_date"], granularity=["day", "hour", "minute"]))) - lines.append(f"*Amount:* {cur_entry_amount:.8g} " + lines.append(f"*Amount:* {round_value(cur_entry_amount, 8)} " f"({fmt_coin(order['cost'], quote_currency)})") - lines.append(f"*Average {wording} Price:* {cur_entry_average:.8g} " + lines.append(f"*Average {wording} Price:* {round_value(cur_entry_average, 8)} " f"({price_to_1st_entry:.2%} from 1st entry rate)") lines.append(f"*Order Filled:* {order['order_filled_date']}") @@ -686,11 +689,11 @@ class Telegram(RPCHandler): ]) lines.extend([ - "*Open Rate:* `{open_rate:.8g}`", - "*Close Rate:* `{close_rate:.8g}`" if r['close_rate'] else "", + f"*Open Rate:* `{round_value(r['open_rate'], 8)}`", + f"*Close Rate:* `{round_value(r['close_rate'], 8)}`" if r['close_rate'] else "", "*Open Date:* `{open_date}`", "*Close Date:* `{close_date}`" if r['close_date'] else "", - " \n*Current Rate:* `{current_rate:.8g}`" if r['is_open'] else "", + f" \n*Current Rate:* `{round_value(r['current_rate'], 8)}`" if r['is_open'] else "", ("*Unrealized Profit:* " if r['is_open'] else "*Close Profit: *") + "`{profit_ratio:.2%}` `({profit_abs_r})`", ]) @@ -711,9 +714,9 @@ class Telegram(RPCHandler): "`({initial_stop_loss_ratio:.2%})`") # Adding stoploss and stoploss percentage only if it is not None - lines.append("*Stoploss:* `{stop_loss_abs:.8g}` " + + lines.append(f"*Stoploss:* `{round_value(r['stop_loss_abs'], 8)}` " + ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else "")) - lines.append("*Stoploss distance:* `{stoploss_current_dist:.8g}` " + lines.append(f"*Stoploss distance:* `{round_value(r['stoploss_current_dist'], 8)}` " "`({stoploss_current_dist_ratio:.2%})`") if r.get('open_orders'): lines.append( @@ -1667,6 +1670,8 @@ class Telegram(RPCHandler): "*/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" + "*/list_custom_data :* `List custom_data for Trade ID & Key combo.`\n" + "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`" "_Statistics_\n" "------------\n" @@ -1689,7 +1694,7 @@ class Telegram(RPCHandler): "*/stats:* `Shows Wins / losses by Sell reason as well as " "Avg. holding durations for buys and sells.`\n" "*/help:* `This help message`\n" - "*/version:* `Show version`" + "*/version:* `Show version`\n" ) await self._send_msg(message, parse_mode=ParseMode.MARKDOWN) @@ -1701,7 +1706,9 @@ class Telegram(RPCHandler): Shows the last process timestamp """ health = self._rpc.health() - message = f"Last process: `{health['last_process_loc']}`" + message = f"Last process: `{health['last_process_loc']}`\n" + message += f"Initial bot start: `{health['bot_start_loc']}`\n" + message += f"Last bot restart: `{health['bot_startup_loc']}`" await self._send_msg(message) @authorized_only @@ -1766,6 +1773,53 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) + @authorized_only + async def _list_custom_data(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /list_custom_data . + List custom_data for specified trade (and key if supplied). + :param bot: telegram bot + :param update: message update + :return: None + """ + try: + if not context.args or len(context.args) == 0: + raise RPCException("Trade-id not set.") + trade_id = int(context.args[0]) + key = None if len(context.args) < 2 else str(context.args[1]) + + results = self._rpc._rpc_list_custom_data(trade_id, key) + messages = [] + if len(results) > 0: + messages.append( + 'Found custom-data entr' + ('ies: ' if len(results) > 1 else 'y: ') + ) + for result in results: + lines = [ + f"*Key:* `{result['cd_key']}`", + f"*ID:* `{result['id']}`", + f"*Trade ID:* `{result['ft_trade_id']}`", + f"*Type:* `{result['cd_type']}`", + f"*Value:* `{result['cd_value']}`", + f"*Create Date:* `{format_date(result['created_at'])}`", + f"*Update Date:* `{format_date(result['updated_at'])}`" + ] + # Filter empty lines using list-comprehension + messages.append("\n".join([line for line in lines if line])) + for msg in messages: + if len(msg) > MAX_MESSAGE_LENGTH: + msg = "Message dropped because length exceeds " + msg += f"maximum allowed characters: {MAX_MESSAGE_LENGTH}" + logger.warning(msg) + await self._send_msg(msg) + else: + message = f"Didn't find any custom-data entries for Trade ID: `{trade_id}`" + message += f" and Key: `{key}`." if key is not None else "" + await self._send_msg(message) + + except RPCException as e: + await self._send_msg(str(e)) + 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: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2630c3547..f8a890d5d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -372,6 +372,19 @@ class IStrategy(ABC, HyperStrategyMixin): """ return True + def order_filled(self, pair: str, trade: Trade, order: Order, + current_time: datetime, **kwargs) -> None: + """ + Called right after an order fills. + Will be called for all order types (entry, exit, stoploss, position adjustment). + :param pair: Pair for trade + :param trade: trade object. + :param order: Order object. + :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 + def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> Optional[float]: """ diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 0b30dbd54..8d4459a3c 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -35,7 +35,7 @@ "project_root = \"somedir/freqtrade\"\n", "i=0\n", "try:\n", - " os.chdirdir(project_root)\n", + " os.chdir(project_root)\n", " assert Path('LICENSE').is_file()\n", "except:\n", " while i<4 and (not Path('LICENSE').is_file()):\n", @@ -181,7 +181,7 @@ "\n", "# if backtest_dir points to a directory, it'll automatically load the last backtest file.\n", "backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n", - "# backtest_dir can also point to a specific file \n", + "# backtest_dir can also point to a specific file\n", "# backtest_dir = config[\"user_data_dir\"] / \"backtest_results/backtest-result-2020-07-01_20-04-22.json\"" ] }, diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index 6fad129c7..541c26e87 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -300,3 +300,17 @@ def leverage(self, pair: str, current_time: datetime, current_rate: float, :return: A leverage amount, which is between 1.0 and max_leverage. """ return 1.0 + + +def order_filled(self, pair: str, trade: 'Trade', order: 'Order', + current_time: datetime, **kwargs) -> None: + """ + Called right after an order fills. + Will be called for all order types (entry, exit, stoploss, position adjustment). + :param pair: Pair for trade + :param trade: trade object. + :param order: Order object. + :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/formatters.py b/freqtrade/util/formatters.py index 1a3d064a9..f9d3db6a1 100644 --- a/freqtrade/util/formatters.py +++ b/freqtrade/util/formatters.py @@ -10,6 +10,15 @@ def decimals_per_coin(coin: str): return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK) +def strip_trailing_zeros(value: str) -> str: + """ + Strip trailing zeros from a string + :param value: Value to be stripped + :return: Stripped value + """ + return value.rstrip('0').rstrip('.') + + def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str: """ Round value to given decimals @@ -20,7 +29,7 @@ def round_value(value: float, decimals: int, keep_trailing_zeros=False) -> str: """ val = f"{value:.{decimals}f}" if not keep_trailing_zeros: - val = val.rstrip('0').rstrip('.') + val = strip_trailing_zeros(val) return val @@ -34,7 +43,6 @@ def fmt_coin( :param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2" :return: Formatted / rounded value (with or without coin name) """ - val = f"{value:.{decimals_per_coin(coin)}f}" val = round_value(value, decimals_per_coin(coin), keep_trailing_zeros) if show_coin_name: val = f"{val} {coin}" diff --git a/freqtrade/util/migrations/binance_mig.py b/freqtrade/util/migrations/binance_mig.py index cc9c451b2..b15e20100 100644 --- a/freqtrade/util/migrations/binance_mig.py +++ b/freqtrade/util/migrations/binance_mig.py @@ -63,7 +63,7 @@ def migrate_binance_futures_data(config: Config): # only act on new futures return - from freqtrade.data.history.idatahandler import get_datahandler + from freqtrade.data.history import get_datahandler dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv']) paircombs = dhc.ohlcv_get_available_data( diff --git a/freqtrade/util/migrations/funding_rate_mig.py b/freqtrade/util/migrations/funding_rate_mig.py index 9fe433b2d..85b66ce3f 100644 --- a/freqtrade/util/migrations/funding_rate_mig.py +++ b/freqtrade/util/migrations/funding_rate_mig.py @@ -2,7 +2,7 @@ import logging from typing import Optional from freqtrade.constants import Config -from freqtrade.data.history.idatahandler import get_datahandler +from freqtrade.data.history import get_datahandler from freqtrade.enums import TradingMode from freqtrade.exchange import Exchange diff --git a/ft_client/LICENSE b/ft_client/LICENSE new file mode 120000 index 000000000..ea5b60640 --- /dev/null +++ b/ft_client/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/ft_client/MANIFEST.in b/ft_client/MANIFEST.in new file mode 100644 index 000000000..cee1ed220 --- /dev/null +++ b/ft_client/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include README.md + +prune tests diff --git a/ft_client/README.md b/ft_client/README.md new file mode 100644 index 000000000..a1921aa24 --- /dev/null +++ b/ft_client/README.md @@ -0,0 +1,7 @@ +# Freqtrade Client + +# ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade_poweredby.svg) + +Provides a minimal rest client for the freqtrade rest api. + +Please check out the [main project](https://github.com/freqtrade/freqtrade) for more information or details. diff --git a/ft_client/freqtrade_client/__init__.py b/ft_client/freqtrade_client/__init__.py new file mode 100644 index 000000000..67cd641c4 --- /dev/null +++ b/ft_client/freqtrade_client/__init__.py @@ -0,0 +1,26 @@ +from freqtrade_client.ft_rest_client import FtRestClient + + +__version__ = '2024.3' + +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, 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) + versionfile = Path('./freqtrade_commit') + if versionfile.is_file(): + __version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}" + except Exception: + pass + +__all__ = ['FtRestClient'] diff --git a/ft_client/freqtrade_client/ft_client.py b/ft_client/freqtrade_client/ft_client.py new file mode 100644 index 000000000..96a7510ff --- /dev/null +++ b/ft_client/freqtrade_client/ft_client.py @@ -0,0 +1,106 @@ +import argparse +import inspect +import json +import logging +import re +import sys +from pathlib import Path +from typing import Any, Dict + +import rapidjson +from freqtrade_client import __version__ +from freqtrade_client.ft_rest_client import FtRestClient + + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +) +logger = logging.getLogger("ft_rest_client") + + +def add_arguments(args: Any = None): + parser = argparse.ArgumentParser() + parser.add_argument("command", + help="Positional argument defining the command to execute.", + nargs="?" + ) + parser.add_argument('-V', '--version', action='version', version=f'%(prog)s {__version__}') + parser.add_argument('--show', + help='Show possible methods with this client', + dest='show', + action='store_true', + default=False + ) + + parser.add_argument('-c', '--config', + help='Specify configuration file (default: %(default)s). ', + dest='config', + type=str, + metavar='PATH', + default='config.json' + ) + + parser.add_argument("command_arguments", + help="Positional arguments for the parameters for [command]", + nargs="*", + default=[] + ) + + pargs = parser.parse_args(args) + return vars(pargs) + + +def load_config(configfile): + file = Path(configfile) + if file.is_file(): + with file.open("r") as f: + config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS | + rapidjson.PM_TRAILING_COMMAS) + return config + else: + logger.warning(f"Could not load config file {file}.") + sys.exit(1) + + +def print_commands(): + # Print dynamic help for the different commands using the commands doc-strings + client = FtRestClient(None) + print("Possible commands:\n") + for x, y in inspect.getmembers(client): + if not x.startswith('_'): + doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip() + print(f"{x}\n\t{doc}\n") + + +def main_exec(args: Dict[str, Any]): + + if args.get("show"): + print_commands() + sys.exit() + + config = load_config(args['config']) + url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1') + port = config.get('api_server', {}).get('listen_port', '8080') + username = config.get('api_server', {}).get('username') + password = config.get('api_server', {}).get('password') + + server_url = f"http://{url}:{port}" + client = FtRestClient(server_url, username, password) + + m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')] + command = args["command"] + if command not in m: + logger.error(f"Command {command} not defined") + print_commands() + return + + print(json.dumps(getattr(client, command)(*args["command_arguments"]))) + + +def main(): + """ + Main entry point for the client + """ + args = add_arguments() + main_exec(args) diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py new file mode 100755 index 000000000..de782ee65 --- /dev/null +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -0,0 +1,424 @@ +""" +A Rest Client for Freqtrade bot + +Should not import anything from freqtrade, +so it can be used as a standalone script, and can be installed independently. +""" + +import json +import logging +from typing import Optional +from urllib.parse import urlencode, urlparse, urlunparse + +import requests +from requests.exceptions import ConnectionError + + +logger = logging.getLogger("ft_rest_client") + + +class FtRestClient: + + def __init__(self, serverurl, username=None, password=None, *, + pool_connections=10, pool_maxsize=10): + + self._serverurl = serverurl + self._session = requests.Session() + + # allow configuration of pool + adapter = requests.adapters.HTTPAdapter( + pool_connections=pool_connections, + pool_maxsize=pool_maxsize + ) + self._session.mount('http://', adapter) + + self._session.auth = (username, password) + + 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}>') + basepath = f"{self._serverurl}/api/v1/{apipath}" + + hd = {"Accept": "application/json", + "Content-Type": "application/json" + } + + # Split url + schema, netloc, path, par, query, fragment = urlparse(basepath) + # URLEncode query string + query = urlencode(params) if params else "" + # recombine url + url = urlunparse((schema, netloc, path, par, query, fragment)) + + try: + resp = self._session.request(method, url, headers=hd, data=json.dumps(data)) + # return resp.text + return resp.json() + except ConnectionError: + logger.warning("Connection error") + + def _get(self, apipath, params: Optional[dict] = None): + return self._call("GET", apipath, params=params) + + def _delete(self, apipath, params: Optional[dict] = None): + return self._call("DELETE", apipath, params=params) + + def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None): + return self._call("POST", apipath, params=params, data=data) + + def start(self): + """Start the bot if it's in the stopped state. + + :return: json object + """ + return self._post("start") + + def stop(self): + """Stop the bot. Use `start` to restart. + + :return: json object + """ + return self._post("stop") + + def stopbuy(self): + """Stop buying (but handle sells gracefully). Use `reload_config` to reset. + + :return: json object + """ + return self._post("stopbuy") + + def reload_config(self): + """Reload configuration. + + :return: json object + """ + return self._post("reload_config") + + def balance(self): + """Get the account balance. + + :return: json object + """ + return self._get("balance") + + def count(self): + """Return the amount of open trades. + + :return: json object + """ + return self._get("count") + + def entries(self, pair=None): + """Returns List of dicts containing all Trades, based on buy tag performance + Can either be average for all pairs or a specific pair provided + + :return: json object + """ + return self._get("entries", params={"pair": pair} if pair else None) + + def exits(self, pair=None): + """Returns List of dicts containing all Trades, based on exit reason performance + Can either be average for all pairs or a specific pair provided + + :return: json object + """ + return self._get("exits", params={"pair": pair} if pair else None) + + def mix_tags(self, pair=None): + """Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance + Can either be average for all pairs or a specific pair provided + + :return: json object + """ + return self._get("mix_tags", params={"pair": pair} if pair else None) + + def locks(self): + """Return current locks + + :return: json object + """ + return self._get("locks") + + def delete_lock(self, lock_id): + """Delete (disable) lock from the database. + + :param lock_id: ID for the lock to delete + :return: json object + """ + return self._delete(f"locks/{lock_id}") + + def daily(self, days=None): + """Return the profits for each day, and amount of trades. + + :return: json object + """ + return self._get("daily", params={"timescale": days} if days else None) + + def weekly(self, weeks=None): + """Return the profits for each week, and amount of trades. + + :return: json object + """ + return self._get("weekly", params={"timescale": weeks} if weeks else None) + + def monthly(self, months=None): + """Return the profits for each month, and amount of trades. + + :return: json object + """ + return self._get("monthly", params={"timescale": months} if months else None) + + def edge(self): + """Return information about edge. + + :return: json object + """ + return self._get("edge") + + def profit(self): + """Return the profit summary. + + :return: json object + """ + return self._get("profit") + + def stats(self): + """Return the stats report (durations, sell-reasons). + + :return: json object + """ + return self._get("stats") + + def performance(self): + """Return the performance of the different coins. + + :return: json object + """ + return self._get("performance") + + def status(self): + """Get the status of open trades. + + :return: json object + """ + return self._get("status") + + def version(self): + """Return the version of the bot. + + :return: json object containing the version + """ + return self._get("version") + + def show_config(self): + """ Returns part of the configuration, relevant for trading operations. + :return: json object containing the version + """ + return self._get("show_config") + + def ping(self): + """simple ping""" + configstatus = self.show_config() + if not configstatus: + return {"status": "not_running"} + elif configstatus['state'] == "running": + return {"status": "pong"} + else: + return {"status": "not_running"} + + def logs(self, limit=None): + """Show latest logs. + + :param limit: Limits log messages to the last logs. No limit to get the entire log. + :return: json object + """ + return self._get("logs", params={"limit": limit} if limit else 0) + + def trades(self, limit=None, offset=None): + """Return trades history, sorted by id + + :param limit: Limits trades to the X last trades. Max 500 trades. + :param offset: Offset by this amount of trades. + :return: json object + """ + params = {} + if limit: + params['limit'] = limit + if offset: + params['offset'] = offset + return self._get("trades", params) + + def trade(self, trade_id): + """Return specific trade + + :param trade_id: Specify which trade to get. + :return: json object + """ + return self._get(f"trade/{trade_id}") + + def delete_trade(self, trade_id): + """Delete trade from the database. + Tries to close open orders. Requires manual handling of this asset on the exchange. + + :param trade_id: Deletes the trade with this ID from the database. + :return: json object + """ + 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. + + :return: json object + """ + return self._get("whitelist") + + def blacklist(self, *args): + """Show the current blacklist. + + :param add: List of coins to add (example: "BNB/BTC") + :return: json object + """ + if not args: + return self._get("blacklist") + else: + return self._post("blacklist", data={"blacklist": args}) + + def forcebuy(self, pair, price=None): + """Buy an asset. + + :param pair: Pair to buy (ETH/BTC) + :param price: Optional - price to buy + :return: json object of the trade + """ + data = {"pair": pair, + "price": price + } + return self._post("forcebuy", data=data) + + def forceenter(self, pair, side, price=None): + """Force entering a trade + + :param pair: Pair to buy (ETH/BTC) + :param side: 'long' or 'short' + :param price: Optional - price to buy + :return: json object of the trade + """ + data = {"pair": pair, + "side": side, + } + if price: + data['price'] = price + return self._post("forceenter", data=data) + + def forceexit(self, tradeid, ordertype=None, amount=None): + """Force-exit a trade. + + :param tradeid: Id of the trade (can be received via status command) + :param ordertype: Order type to use (must be market or limit) + :param amount: Amount to sell. Full sell if not given + :return: json object + """ + + return self._post("forceexit", data={ + "tradeid": tradeid, + "ordertype": ordertype, + "amount": amount, + }) + + def strategies(self): + """Lists available strategies + + :return: json object + """ + return self._get("strategies") + + def strategy(self, strategy): + """Get strategy details + + :param strategy: Strategy class name + :return: json object + """ + return self._get(f"strategy/{strategy}") + + def pairlists_available(self): + """Lists available pairlist providers + + :return: json object + """ + return self._get("pairlists/available") + + def plot_config(self): + """Return plot configuration if the strategy defines one. + + :return: json object + """ + return self._get("plot_config") + + def available_pairs(self, timeframe=None, stake_currency=None): + """Return available pair (backtest data) based on timeframe / stake_currency selection + + :param timeframe: Only pairs with this timeframe available. + :param stake_currency: Only pairs that include this timeframe + :return: json object + """ + return self._get("available_pairs", params={ + "stake_currency": stake_currency if timeframe else '', + "timeframe": timeframe if timeframe else '', + }) + + def pair_candles(self, pair, timeframe, limit=None): + """Return live dataframe for . + + :param pair: Pair to get data for + :param timeframe: Only pairs with this timeframe available. + :param limit: Limit result to the last n candles. + :return: json object + """ + params = { + "pair": pair, + "timeframe": timeframe, + } + if limit: + params['limit'] = limit + return self._get("pair_candles", params=params) + + 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 + """ + return self._get("pair_history", params={ + "pair": pair, + "timeframe": timeframe, + "strategy": strategy, + "freqaimodel": freqaimodel, + "timerange": timerange if timerange else '', + }) + + def sysinfo(self): + """Provides system information (CPU, RAM usage) + + :return: json object + """ + return self._get("sysinfo") + + def health(self): + """Provides a quick health check of the running bot. + + :return: json object + """ + return self._get("health") diff --git a/ft_client/pyproject.toml b/ft_client/pyproject.toml new file mode 100644 index 000000000..919e524f8 --- /dev/null +++ b/ft_client/pyproject.toml @@ -0,0 +1,54 @@ +[build-system] +requires = ["setuptools >= 64.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "freqtrade-client" +dynamic = ["version"] + +authors = [ + {name = "Freqtrade Team"}, + {name = "Freqtrade Team", email = "freqtrade@protonmail.com"}, +] + +description = "Freqtrade - Client scripts" +readme = "README.md" +requires-python = ">=3.9" +license = {text = "GPLv3"} +# license = "GPLv3" +classifiers = [ + "Environment :: Console", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: MacOS", + "Operating System :: Unix", + "Topic :: Office/Business :: Financial :: Investment", +] + +dependencies = [ + 'requests >= 2.26.0', + 'python-rapidjson >= 1.0', +] + + +[project.urls] +Homepage = "https://github.com/freqtrade/freqtrade" +Documentation = "https://freqtrade.io" +"Bug Tracker" = "https://github.com/freqtrade/freqtrade/issues" + + +[project.scripts] +freqtrade-client = "freqtrade_client.ft_client:main" + +[tool.setuptools.packages.find] +where = ["."] +include = ["freqtrade_client*"] +exclude = ["tests", "tests.*"] +namespaces = true + +[tool.setuptools.dynamic] +version = {attr = "freqtrade_client.__version__"} diff --git a/ft_client/requirements.txt b/ft_client/requirements.txt new file mode 100644 index 000000000..56def4059 --- /dev/null +++ b/ft_client/requirements.txt @@ -0,0 +1,3 @@ +# Requirements for freqtrade client library +requests==2.31.0 +python-rapidjson==1.16 diff --git a/ft_client/test_client/__init__.py b/ft_client/test_client/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py new file mode 100644 index 000000000..e7c2f32e6 --- /dev/null +++ b/ft_client/test_client/test_rest_client.py @@ -0,0 +1,152 @@ +import re +from unittest.mock import MagicMock + +import pytest +from freqtrade_client import FtRestClient +from freqtrade_client.ft_client import add_arguments, main_exec +from requests.exceptions import ConnectionError + + +def log_has_re(line, logs): + """Check if line matches some caplog's message.""" + return any(re.match(line, message) for message in logs.messages) + + +def get_rest_client(): + client = FtRestClient('http://localhost:8080', 'freqtrader', 'password') + client._session = MagicMock() + request_mock = MagicMock() + client._session.request = request_mock + return client, request_mock + + +def test_FtRestClient_init(): + client = FtRestClient('http://localhost:8080', 'freqtrader', 'password') + assert client is not None + assert client._serverurl == 'http://localhost:8080' + assert client._session is not None + assert client._session.auth is not None + assert client._session.auth == ('freqtrader', 'password') + + +@pytest.mark.parametrize('method', ['GET', 'POST', 'DELETE']) +def test_FtRestClient_call(method): + client, mock = get_rest_client() + client._call(method, '/dummytest') + assert mock.call_count == 1 + + getattr(client, f"_{method.lower()}")('/dummytest') + assert mock.call_count == 2 + + +def test_FtRestClient_call_invalid(caplog): + client, _ = get_rest_client() + with pytest.raises(ValueError): + client._call('PUTTY', '/dummytest') + + client._session.request = MagicMock(side_effect=ConnectionError()) + client._call('GET', '/dummytest') + + assert log_has_re('Connection error', caplog) + + +@pytest.mark.parametrize('method,args', [ + ('start', []), + ('stop', []), + ('stopbuy', []), + ('reload_config', []), + ('balance', []), + ('count', []), + ('entries', []), + ('exits', []), + ('mix_tags', []), + ('locks', []), + ('delete_lock', [2]), + ('daily', []), + ('daily', [15]), + ('weekly', []), + ('weekly', [15]), + ('monthly', []), + ('monthly', [12]), + ('edge', []), + ('profit', []), + ('stats', []), + ('performance', []), + ('status', []), + ('version', []), + ('show_config', []), + ('ping', []), + ('logs', []), + ('logs', [55]), + ('trades', []), + ('trades', [5]), + ('trades', [5, 5]), # With offset + ('trade', [1]), + ('delete_trade', [1]), + ('cancel_open_order', [1]), + ('whitelist', []), + ('blacklist', []), + ('blacklist', ['XRP/USDT']), + ('blacklist', ['XRP/USDT', 'BTC/USDT']), + ('forcebuy', ['XRP/USDT']), + ('forcebuy', ['XRP/USDT', 1.5]), + ('forceenter', ['XRP/USDT', 'short']), + ('forceenter', ['XRP/USDT', 'short', 1.5]), + ('forceexit', [1]), + ('forceexit', [1, 'limit']), + ('forceexit', [1, 'limit', 100]), + ('strategies', []), + ('strategy', ['sampleStrategy']), + ('pairlists_available', []), + ('plot_config', []), + ('available_pairs', []), + ('available_pairs', ['5m']), + ('pair_candles', ['XRP/USDT', '5m']), + ('pair_candles', ['XRP/USDT', '5m', 500]), + ('pair_history', ['XRP/USDT', '5m', 'SampleStrategy']), + ('sysinfo', []), + ('health', []), +]) +def test_FtRestClient_call_explicit_methods(method, args): + client, mock = get_rest_client() + exec = getattr(client, method) + exec(*args) + assert mock.call_count == 1 + + +def test_ft_client(mocker, capsys, caplog): + with pytest.raises(SystemExit): + args = add_arguments(['-V']) + + args = add_arguments(['--show']) + assert isinstance(args, dict) + assert args['show'] is True + with pytest.raises(SystemExit): + main_exec(args) + captured = capsys.readouterr() + assert 'Possible commands' in captured.out + + mock = mocker.patch('freqtrade_client.ft_client.FtRestClient._call') + args = add_arguments([ + '--config', + 'tests/testdata/testconfigs/main_test_config.json', + 'ping' + ]) + main_exec(args) + captured = capsys.readouterr() + assert mock.call_count == 1 + + with pytest.raises(SystemExit): + args = add_arguments(['--config', 'tests/testdata/testconfigs/nonexisting.json']) + main_exec(args) + + assert log_has_re(r'Could not load config file .*nonexisting\.json\.', + caplog) + + args = add_arguments([ + '--config', + 'tests/testdata/testconfigs/main_test_config.json', + 'whatever' + ]) + main_exec(args) + assert log_has_re('Command whatever not defined', caplog) diff --git a/pyproject.toml b/pyproject.toml index 753f44262..f7a244c8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Operating System :: MacOS", "Operating System :: Unix", "Topic :: Office/Business :: Financial :: Investment", @@ -45,7 +46,7 @@ zip-safe = false [tool.setuptools.packages.find] where = ["."] include = ["freqtrade*"] -exclude = ["tests", "tests.*"] +exclude = ["tests", "tests.*", "user_data", "user_data*"] namespaces = true [tool.setuptools.dynamic] diff --git a/requirements-dev.txt b/requirements-dev.txt index 77d981087..9fbb7b667 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,25 +7,25 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.2.2 -mypy==1.8.0 -pre-commit==3.6.2 -pytest==8.0.2 -pytest-asyncio==0.23.5 -pytest-cov==4.1.0 -pytest-mock==3.12.0 +ruff==0.3.4 +mypy==1.9.0 +pre-commit==3.7.0 +pytest==8.1.1 +pytest-asyncio==0.23.6 +pytest-cov==5.0.0 +pytest-mock==3.14.0 pytest-random-order==1.1.1 pytest-xdist==3.5.0 isort==5.13.2 # For datetime mocking -time-machine==2.13.0 +time-machine==2.14.1 # Convert jupyter notebooks to markdown documents -nbconvert==7.16.1 +nbconvert==7.16.3 # mypy types types-cachetools==5.3.0.7 types-filelock==3.2.7 -types-requests==2.31.0.20240218 +types-requests==2.31.0.20240311 types-tabulate==0.9.0.20240106 -types-python-dateutil==2.8.19.20240106 +types-python-dateutil==2.9.0.20240316 diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 67fed9190..3f623b0c0 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -2,10 +2,9 @@ -r requirements-freqai.txt # Required for freqai-rl -torch==2.1.2; python_version < '3.12' -#until these branches will be released we can use this -gymnasium==0.29.1; python_version < '3.12' -stable_baselines3==2.2.1; python_version < '3.12' -sb3_contrib>=2.0.0a9; python_version < '3.12' +torch==2.2.1 +gymnasium==0.29.1 +stable_baselines3==2.2.1 +sb3_contrib>=2.2.1 # Progress bar for stable-baselines3 and sb3-contrib tqdm==4.66.2 diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 0532562da..31366efa7 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -5,7 +5,7 @@ # Required for freqai scikit-learn==1.4.1.post1 joblib==1.3.2 -catboost==1.2.2; 'arm' not in platform_machine and python_version < '3.12' +catboost==1.2.3; 'arm' not in platform_machine lightgbm==4.3.0 xgboost==2.0.3 tensorboard==2.16.2 diff --git a/requirements-plot.txt b/requirements-plot.txt index af746ef98..0d3c404f5 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.19.0 +plotly==5.20.0 diff --git a/requirements.txt b/requirements.txt index 94f63d033..9f6505618 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ numpy==1.26.4 -pandas==2.1.4 +pandas==2.2.1 pandas-ta==0.3.14b -ccxt==4.2.51 +ccxt==4.2.82 cryptography==42.0.5 aiohttp==3.9.3 -SQLAlchemy==2.0.27 -python-telegram-bot==20.8 +SQLAlchemy==2.0.29 +python-telegram-bot==21.0.1 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 arrow==1.3.0 -cachetools==5.3.2 +cachetools==5.3.3 requests==2.31.0 urllib3==2.2.1 jsonschema==4.21.1 @@ -21,14 +21,14 @@ pycoingecko==3.1.0 jinja2==3.1.3 tables==3.9.1 joblib==1.3.2 -rich==13.7.0 -pyarrow==15.0.0; platform_machine != 'armv7l' +rich==13.7.1 +pyarrow==15.0.2; platform_machine != 'armv7l' # find first, C search in arrays py_find_1st==1.1.6 # Load ticker files 30% faster -python-rapidjson==1.14 +python-rapidjson==1.16 # Properly format api responses orjson==3.9.15 @@ -37,8 +37,8 @@ sdnotify==0.3.2 # API Server fastapi==0.110.0 -pydantic==2.6.2 -uvicorn==0.27.1 +pydantic==2.6.4 +uvicorn==0.29.0 pyjwt==2.8.0 aiofiles==23.2.1 psutil==5.9.8 @@ -49,7 +49,7 @@ colorama==0.4.6 questionary==2.0.1 prompt-toolkit==3.0.36 # Extensions to datetime library -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 pytz==2024.1 #Futures @@ -60,4 +60,4 @@ websockets==12.0 janus==1.0.0 ast-comments==1.2.1 -packaging==23.2 +packaging==24.0 diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 5970b0c5b..c22dd18ae 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -7,505 +7,8 @@ Should not import anything from freqtrade, so it can be used as a standalone script. """ -import argparse -import inspect -import json -import logging -import re -import sys -from pathlib import Path -from typing import Optional -from urllib.parse import urlencode, urlparse, urlunparse +from freqtrade_client.ft_client import main -import rapidjson -import requests -from requests.exceptions import ConnectionError - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', -) -logger = logging.getLogger("ft_rest_client") - - -class FtRestClient: - - def __init__(self, serverurl, username=None, password=None): - - self._serverurl = serverurl - self._session = requests.Session() - self._session.auth = (username, password) - - 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}>') - basepath = f"{self._serverurl}/api/v1/{apipath}" - - hd = {"Accept": "application/json", - "Content-Type": "application/json" - } - - # Split url - schema, netloc, path, par, query, fragment = urlparse(basepath) - # URLEncode query string - query = urlencode(params) if params else "" - # recombine url - url = urlunparse((schema, netloc, path, par, query, fragment)) - - try: - resp = self._session.request(method, url, headers=hd, data=json.dumps(data)) - # return resp.text - return resp.json() - except ConnectionError: - logger.warning("Connection error") - - def _get(self, apipath, params: Optional[dict] = None): - return self._call("GET", apipath, params=params) - - def _delete(self, apipath, params: Optional[dict] = None): - return self._call("DELETE", apipath, params=params) - - def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None): - return self._call("POST", apipath, params=params, data=data) - - def start(self): - """Start the bot if it's in the stopped state. - - :return: json object - """ - return self._post("start") - - def stop(self): - """Stop the bot. Use `start` to restart. - - :return: json object - """ - return self._post("stop") - - def stopbuy(self): - """Stop buying (but handle sells gracefully). Use `reload_config` to reset. - - :return: json object - """ - return self._post("stopbuy") - - def reload_config(self): - """Reload configuration. - - :return: json object - """ - return self._post("reload_config") - - def balance(self): - """Get the account balance. - - :return: json object - """ - return self._get("balance") - - def count(self): - """Return the amount of open trades. - - :return: json object - """ - return self._get("count") - - def entries(self, pair=None): - """Returns List of dicts containing all Trades, based on buy tag performance - Can either be average for all pairs or a specific pair provided - - :return: json object - """ - return self._get("entries", params={"pair": pair} if pair else None) - - def exits(self, pair=None): - """Returns List of dicts containing all Trades, based on exit reason performance - Can either be average for all pairs or a specific pair provided - - :return: json object - """ - return self._get("exits", params={"pair": pair} if pair else None) - - def mix_tags(self, pair=None): - """Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance - Can either be average for all pairs or a specific pair provided - - :return: json object - """ - return self._get("mix_tags", params={"pair": pair} if pair else None) - - def locks(self): - """Return current locks - - :return: json object - """ - return self._get("locks") - - def delete_lock(self, lock_id): - """Delete (disable) lock from the database. - - :param lock_id: ID for the lock to delete - :return: json object - """ - return self._delete(f"locks/{lock_id}") - - def daily(self, days=None): - """Return the profits for each day, and amount of trades. - - :return: json object - """ - return self._get("daily", params={"timescale": days} if days else None) - - def weekly(self, weeks=None): - """Return the profits for each week, and amount of trades. - - :return: json object - """ - return self._get("weekly", params={"timescale": weeks} if weeks else None) - - def monthly(self, months=None): - """Return the profits for each month, and amount of trades. - - :return: json object - """ - return self._get("monthly", params={"timescale": months} if months else None) - - def edge(self): - """Return information about edge. - - :return: json object - """ - return self._get("edge") - - def profit(self): - """Return the profit summary. - - :return: json object - """ - return self._get("profit") - - def stats(self): - """Return the stats report (durations, sell-reasons). - - :return: json object - """ - return self._get("stats") - - def performance(self): - """Return the performance of the different coins. - - :return: json object - """ - return self._get("performance") - - def status(self): - """Get the status of open trades. - - :return: json object - """ - return self._get("status") - - def version(self): - """Return the version of the bot. - - :return: json object containing the version - """ - return self._get("version") - - def show_config(self): - """ Returns part of the configuration, relevant for trading operations. - :return: json object containing the version - """ - return self._get("show_config") - - def ping(self): - """simple ping""" - configstatus = self.show_config() - if not configstatus: - return {"status": "not_running"} - elif configstatus['state'] == "running": - return {"status": "pong"} - else: - return {"status": "not_running"} - - def logs(self, limit=None): - """Show latest logs. - - :param limit: Limits log messages to the last logs. No limit to get the entire log. - :return: json object - """ - return self._get("logs", params={"limit": limit} if limit else 0) - - def trades(self, limit=None, offset=None): - """Return trades history, sorted by id - - :param limit: Limits trades to the X last trades. Max 500 trades. - :param offset: Offset by this amount of trades. - :return: json object - """ - params = {} - if limit: - params['limit'] = limit - if offset: - params['offset'] = offset - return self._get("trades", params) - - def trade(self, trade_id): - """Return specific trade - - :param trade_id: Specify which trade to get. - :return: json object - """ - return self._get(f"trade/{trade_id}") - - def delete_trade(self, trade_id): - """Delete trade from the database. - Tries to close open orders. Requires manual handling of this asset on the exchange. - - :param trade_id: Deletes the trade with this ID from the database. - :return: json object - """ - 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. - - :return: json object - """ - return self._get("whitelist") - - def blacklist(self, *args): - """Show the current blacklist. - - :param add: List of coins to add (example: "BNB/BTC") - :return: json object - """ - if not args: - return self._get("blacklist") - else: - return self._post("blacklist", data={"blacklist": args}) - - def forcebuy(self, pair, price=None): - """Buy an asset. - - :param pair: Pair to buy (ETH/BTC) - :param price: Optional - price to buy - :return: json object of the trade - """ - data = {"pair": pair, - "price": price - } - return self._post("forcebuy", data=data) - - def forceenter(self, pair, side, price=None): - """Force entering a trade - - :param pair: Pair to buy (ETH/BTC) - :param side: 'long' or 'short' - :param price: Optional - price to buy - :return: json object of the trade - """ - data = {"pair": pair, - "side": side, - } - if price: - data['price'] = price - return self._post("forceenter", data=data) - - def forceexit(self, tradeid, ordertype=None, amount=None): - """Force-exit a trade. - - :param tradeid: Id of the trade (can be received via status command) - :param ordertype: Order type to use (must be market or limit) - :param amount: Amount to sell. Full sell if not given - :return: json object - """ - - return self._post("forceexit", data={ - "tradeid": tradeid, - "ordertype": ordertype, - "amount": amount, - }) - - def strategies(self): - """Lists available strategies - - :return: json object - """ - return self._get("strategies") - - def strategy(self, strategy): - """Get strategy details - - :param strategy: Strategy class name - :return: json object - """ - return self._get(f"strategy/{strategy}") - - def pairlists_available(self): - """Lists available pairlist providers - - :return: json object - """ - return self._get("pairlists/available") - - def plot_config(self): - """Return plot configuration if the strategy defines one. - - :return: json object - """ - return self._get("plot_config") - - def available_pairs(self, timeframe=None, stake_currency=None): - """Return available pair (backtest data) based on timeframe / stake_currency selection - - :param timeframe: Only pairs with this timeframe available. - :param stake_currency: Only pairs that include this timeframe - :return: json object - """ - return self._get("available_pairs", params={ - "stake_currency": stake_currency if timeframe else '', - "timeframe": timeframe if timeframe else '', - }) - - def pair_candles(self, pair, timeframe, limit=None): - """Return live dataframe for . - - :param pair: Pair to get data for - :param timeframe: Only pairs with this timeframe available. - :param limit: Limit result to the last n candles. - :return: json object - """ - params = { - "pair": pair, - "timeframe": timeframe, - } - if limit: - params['limit'] = limit - return self._get("pair_candles", params=params) - - 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 - """ - return self._get("pair_history", params={ - "pair": pair, - "timeframe": timeframe, - "strategy": strategy, - "freqaimodel": freqaimodel, - "timerange": timerange if timerange else '', - }) - - def sysinfo(self): - """Provides system information (CPU, RAM usage) - - :return: json object - """ - return self._get("sysinfo") - - def health(self): - """Provides a quick health check of the running bot. - - :return: json object - """ - return self._get("health") - - -def add_arguments(): - parser = argparse.ArgumentParser() - parser.add_argument("command", - help="Positional argument defining the command to execute.", - nargs="?" - ) - - parser.add_argument('--show', - help='Show possible methods with this client', - dest='show', - action='store_true', - default=False - ) - - parser.add_argument('-c', '--config', - help='Specify configuration file (default: %(default)s). ', - dest='config', - type=str, - metavar='PATH', - default='config.json' - ) - - parser.add_argument("command_arguments", - help="Positional arguments for the parameters for [command]", - nargs="*", - default=[] - ) - - args = parser.parse_args() - return vars(args) - - -def load_config(configfile): - file = Path(configfile) - if file.is_file(): - with file.open("r") as f: - config = rapidjson.load(f, parse_mode=rapidjson.PM_COMMENTS | - rapidjson.PM_TRAILING_COMMAS) - return config - else: - logger.warning(f"Could not load config file {file}.") - sys.exit(1) - - -def print_commands(): - # Print dynamic help for the different commands using the commands doc-strings - client = FtRestClient(None) - print("Possible commands:\n") - for x, y in inspect.getmembers(client): - if not x.startswith('_'): - doc = re.sub(':return:.*', '', getattr(client, x).__doc__, flags=re.MULTILINE).rstrip() - print(f"{x}\n\t{doc}\n") - - -def main(args): - - if args.get("show"): - print_commands() - sys.exit() - - config = load_config(args['config']) - url = config.get('api_server', {}).get('listen_ip_address', '127.0.0.1') - port = config.get('api_server', {}).get('listen_port', '8080') - username = config.get('api_server', {}).get('username') - password = config.get('api_server', {}).get('password') - - server_url = f"http://{url}:{port}" - client = FtRestClient(server_url, username, password) - - m = [x for x, y in inspect.getmembers(client) if not x.startswith('_')] - command = args["command"] - if command not in m: - logger.error(f"Command {command} not defined") - print_commands() - return - - print(json.dumps(getattr(client, command)(*args["command_arguments"]))) - - -if __name__ == "__main__": - args = add_arguments() - main(args) +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index dea1966fa..f292a5349 100644 --- a/setup.py +++ b/setup.py @@ -112,6 +112,7 @@ setup( 'python-dateutil', 'pytz', 'packaging', + 'freqtrade-client', ], extras_require={ 'dev': all_extra, diff --git a/setup.sh b/setup.sh index 6bf85edab..68374a689 100755 --- a/setup.sh +++ b/setup.sh @@ -161,7 +161,7 @@ function install_macos() { /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" fi - brew install gettext + brew install gettext libomp #Gets number after decimal in python version version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g') diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index cdad46407..789535a46 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -12,9 +12,9 @@ from freqtrade.commands import (start_backtesting_show, start_convert_data, star start_create_userdir, start_download_data, start_hyperopt_list, 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_strategy_update, start_test_pairlist, start_trading, - start_webserver) + start_list_timeframes, start_new_strategy, start_show_config, + start_show_trades, 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) @@ -39,6 +39,14 @@ def test_setup_utils_configuration(): assert "exchange" in config assert config['dry_run'] is True + args = [ + 'list-exchanges', '--config', 'tests/testdata/testconfigs/testconfig.json', + ] + + config = setup_utils_configuration(get_args(args), RunMode.OTHER, set_dry=False) + assert "exchange" in config + assert config['dry_run'] is False + def test_start_trading_fail(mocker, caplog): @@ -51,15 +59,16 @@ def test_start_trading_fail(mocker, caplog): 'trade', '-c', 'tests/testdata/testconfigs/main_test_config.json' ] - start_trading(get_args(args)) + with pytest.raises(OperationalException): + start_trading(get_args(args)) assert exitmock.call_count == 1 exitmock.reset_mock() caplog.clear() mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(side_effect=OperationalException)) - start_trading(get_args(args)) + with pytest.raises(OperationalException): + start_trading(get_args(args)) assert exitmock.call_count == 0 - assert log_has('Fatal exception!', caplog) def test_start_webserver(mocker, caplog): @@ -820,11 +829,6 @@ def test_download_data_trades(mocker): "--trading-mode", "futures", "--dl-trades" ] - with pytest.raises(OperationalException, - match="Trade download not supported for futures."): - pargs = get_args(args) - pargs['config'] = None - start_download_data(pargs) def test_download_data_data_invalid(mocker): @@ -842,10 +846,11 @@ def test_download_data_data_invalid(mocker): start_download_data(pargs) -def test_start_convert_trades(mocker, caplog): +def test_start_convert_trades(mocker): convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) + mocker.patch(f'{EXMS}.get_markets') mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "trades-to-ohlcv", @@ -1575,3 +1580,33 @@ def test_start_strategy_updater(mocker, tmp_path): start_strategy_update(pargs) # Number of strategies in the test directory assert sc_mock.call_count == 2 + + +def test_start_show_config(capsys, caplog): + args = [ + "show-config", + "--config", + "tests/testdata/testconfigs/main_test_config.json", + ] + pargs = get_args(args) + start_show_config(pargs) + + captured = capsys.readouterr() + assert "Your combined configuration is:" in captured.out + assert '"max_open_trades":' in captured.out + assert '"secret": "REDACTED"' in captured.out + + args = [ + "show-config", + "--config", + "tests/testdata/testconfigs/main_test_config.json", + "--show-sensitive" + ] + pargs = get_args(args) + start_show_config(pargs) + + captured = capsys.readouterr() + assert "Your combined configuration is:" in captured.out + assert '"max_open_trades":' in captured.out + assert '"secret": "REDACTED"' not in captured.out + assert log_has_re(r'Sensitive information will be shown in the upcomming output.*', caplog) diff --git a/tests/conftest.py b/tests/conftest.py index 9c81c050d..d894a7908 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -142,8 +142,8 @@ def generate_trades_history(n_rows, start_date: Optional[datetime] = None, days= return df -def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): - np.random.seed(42) +def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05', random_seed=42): + np.random.seed(random_seed) base = np.random.normal(20, 2, size=size) if timeframe == '1y': @@ -174,10 +174,10 @@ def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): return df -def generate_test_data_raw(timeframe: str, size: int, start: str = '2020-07-05'): +def generate_test_data_raw(timeframe: str, size: int, start: str = '2020-07-05', random_seed=42): """ Generates data in the ohlcv format used by ccxt """ - df = generate_test_data(timeframe, size, start) - df['date'] = df.loc[:, 'date'].view(np.int64) // 1000 // 1000 + df = generate_test_data(timeframe, size, start, random_seed) + df['date'] = df.loc[:, 'date'].astype(np.int64) // 1000 // 1000 return list(list(x) for x in zip(*(df[x].values.tolist() for x in df.columns))) diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 08fc785aa..d6f88405b 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -15,7 +15,7 @@ from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_forma trades_to_ohlcv, trim_dataframe) from freqtrade.data.history import (get_timerange, load_data, load_pair_history, validate_backtest_data) -from freqtrade.data.history.idatahandler import IDataHandler +from freqtrade.data.history.datahandlers import IDataHandler from freqtrade.enums import CandleType from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from tests.conftest import generate_test_data, generate_trades_history, log_has, log_has_re @@ -542,7 +542,9 @@ def test_convert_trades_to_ohlcv(testdatadir, tmp_path, caplog): convert_trades_to_ohlcv([pair], timeframes=['1m', '5m'], data_format_trades='jsongz', - datadir=tmp_path, timerange=tr, erase=True) + datadir=tmp_path, timerange=tr, erase=True, + data_format_ohlcv='feather', + candle_type=CandleType.SPOT) assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) # Load new data @@ -556,5 +558,7 @@ def test_convert_trades_to_ohlcv(testdatadir, tmp_path, caplog): convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'], data_format_trades='jsongz', - datadir=tmp_path, timerange=tr, erase=True) + datadir=tmp_path, timerange=tr, erase=True, + data_format_ohlcv='feather', + candle_type=CandleType.SPOT) assert log_has(msg, caplog) diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index a0a37c393..25854b261 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -11,11 +11,12 @@ from pandas.testing import assert_frame_equal from freqtrade.configuration import TimeRange from freqtrade.constants import AVAILABLE_DATAHANDLERS -from freqtrade.data.history.featherdatahandler import FeatherDataHandler -from freqtrade.data.history.hdf5datahandler import HDF5DataHandler -from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler, get_datahandlerclass -from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHandler -from freqtrade.data.history.parquetdatahandler import ParquetDataHandler +from freqtrade.data.history.datahandlers.featherdatahandler import FeatherDataHandler +from freqtrade.data.history.datahandlers.hdf5datahandler import HDF5DataHandler +from freqtrade.data.history.datahandlers.idatahandler import (IDataHandler, get_datahandler, + get_datahandlerclass) +from freqtrade.data.history.datahandlers.jsondatahandler import JsonDataHandler, JsonGzDataHandler +from freqtrade.data.history.datahandlers.parquetdatahandler import ParquetDataHandler from freqtrade.enums import CandleType, TradingMode from tests.conftest import log_has, log_has_re @@ -261,11 +262,11 @@ def test_datahandler_trades_not_supported(datahandler, testdatadir, ): def test_jsondatahandler_trades_load(testdatadir, caplog): dh = JsonGzDataHandler(testdatadir) logmsg = "Old trades format detected - converting" - dh.trades_load('XRP/ETH') + dh.trades_load('XRP/ETH', TradingMode.SPOT) assert not log_has(logmsg, caplog) # Test conversation is happening - dh.trades_load('XRP/OLD') + dh.trades_load('XRP/OLD', TradingMode.SPOT) assert log_has(logmsg, caplog) @@ -300,16 +301,16 @@ def test_datahandler_trades_get_pairs(testdatadir, datahandler, expected): def test_hdf5datahandler_trades_load(testdatadir): dh = get_datahandler(testdatadir, 'hdf5') - trades = dh.trades_load('XRP/ETH') + trades = dh.trades_load('XRP/ETH', TradingMode.SPOT) assert isinstance(trades, DataFrame) - trades1 = dh.trades_load('UNITTEST/NONEXIST') + trades1 = dh.trades_load('UNITTEST/NONEXIST', TradingMode.SPOT) assert isinstance(trades1, DataFrame) assert trades1.empty # data goes from 2019-10-11 - 2019-10-13 timerange = TimeRange.parse_timerange('20191011-20191012') - trades2 = dh._trades_load('XRP/ETH', timerange) + trades2 = dh._trades_load('XRP/ETH', TradingMode.SPOT, timerange) assert len(trades) > len(trades2) # Check that ID is None (If it's nan, it's wrong) assert trades2.iloc[0]['type'] is None @@ -451,13 +452,13 @@ def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir): @pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet']) def test_datahandler_trades_load(testdatadir, datahandler): dh = get_datahandler(testdatadir, datahandler) - trades = dh.trades_load('XRP/ETH') + trades = dh.trades_load('XRP/ETH', TradingMode.SPOT) assert isinstance(trades, DataFrame) assert trades.iloc[0]['timestamp'] == 1570752011620 assert trades.iloc[0]['date'] == Timestamp('2019-10-11 00:00:11.620000+0000') assert trades.iloc[-1]['cost'] == 0.1986231 - trades1 = dh.trades_load('UNITTEST/NONEXIST') + trades1 = dh.trades_load('UNITTEST/NONEXIST', TradingMode.SPOT) assert isinstance(trades, DataFrame) assert trades1.empty @@ -465,15 +466,15 @@ def test_datahandler_trades_load(testdatadir, datahandler): @pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet']) def test_datahandler_trades_store(testdatadir, tmp_path, datahandler): dh = get_datahandler(testdatadir, datahandler) - trades = dh.trades_load('XRP/ETH') + trades = dh.trades_load('XRP/ETH', TradingMode.SPOT) dh1 = get_datahandler(tmp_path, datahandler) - dh1.trades_store('XRP/NEW', trades) + dh1.trades_store('XRP/NEW', trades, TradingMode.SPOT) file = tmp_path / f'XRP_NEW-trades.{dh1._get_file_extension()}' assert file.is_file() # Load trades back - trades_new = dh1.trades_load('XRP/NEW') + trades_new = dh1.trades_load('XRP/NEW', TradingMode.SPOT) assert_frame_equal(trades, trades_new, check_exact=True) assert len(trades_new) == len(trades) @@ -483,11 +484,11 @@ def test_datahandler_trades_purge(mocker, testdatadir, datahandler): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) dh = get_datahandler(testdatadir, datahandler) - assert not dh.trades_purge('UNITTEST/NONEXIST') + assert not dh.trades_purge('UNITTEST/NONEXIST', TradingMode.SPOT) assert unlinkmock.call_count == 0 mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - assert dh.trades_purge('UNITTEST/NONEXIST') + assert dh.trades_purge('UNITTEST/NONEXIST', TradingMode.SPOT) assert unlinkmock.call_count == 1 diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index cc875b970..575d2903b 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -64,9 +64,12 @@ def test_historic_ohlcv(mocker, default_conf, ohlcv_history): def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history): hdf5loadmock = MagicMock(return_value=ohlcv_history) featherloadmock = MagicMock(return_value=ohlcv_history) - mocker.patch("freqtrade.data.history.hdf5datahandler.HDF5DataHandler._ohlcv_load", hdf5loadmock) - mocker.patch("freqtrade.data.history.featherdatahandler.FeatherDataHandler._ohlcv_load", - featherloadmock) + mocker.patch( + "freqtrade.data.history.datahandlers.hdf5datahandler.HDF5DataHandler._ohlcv_load", + hdf5loadmock) + mocker.patch( + "freqtrade.data.history.datahandlers.featherdatahandler.FeatherDataHandler._ohlcv_load", + featherloadmock) default_conf["runmode"] = RunMode.BACKTEST exchange = get_patched_exchange(mocker, default_conf) diff --git a/tests/data/test_download_data.py b/tests/data/test_download_data.py index 97640d01c..1518b28f3 100644 --- a/tests/data/test_download_data.py +++ b/tests/data/test_download_data.py @@ -78,10 +78,6 @@ def test_download_data_main_trades(mocker): "trading_mode": "futures", }) - with pytest.raises(OperationalException, - match="Trade download not supported for futures."): - download_data_main(config) - def test_download_data_main_data_invalid(mocker): patch_exchange(mocker, id="kraken") diff --git a/tests/data/test_history.py b/tests/data/test_history.py index a48d34aee..f95b05835 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -15,15 +15,15 @@ from pandas.testing import assert_frame_equal from freqtrade.configuration import TimeRange from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data.converter import ohlcv_to_dataframe +from freqtrade.data.history import get_datahandler +from freqtrade.data.history.datahandlers.jsondatahandler import JsonDataHandler, JsonGzDataHandler from freqtrade.data.history.history_utils import (_download_pair_history, _download_trades_history, _load_cached_data_for_updating, get_timerange, load_data, load_pair_history, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, refresh_data, validate_backtest_data) -from freqtrade.data.history.idatahandler import get_datahandler -from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHandler -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver @@ -91,7 +91,7 @@ def test_load_data_mark(ohlcv_history, mocker, caplog, testdatadir) -> None: def test_load_data_startup_candles(mocker, testdatadir) -> None: ltfmock = mocker.patch( - 'freqtrade.data.history.featherdatahandler.FeatherDataHandler._ohlcv_load', + 'freqtrade.data.history.datahandlers.featherdatahandler.FeatherDataHandler._ohlcv_load', MagicMock(return_value=DataFrame())) timerange = TimeRange('date', None, 1510639620, 0) load_pair_history(pair='UNITTEST/BTC', timeframe='1m', @@ -168,20 +168,21 @@ def test_json_pair_data_filename(pair, timeframe, expected_result, candle_type): assert fn == Path(expected_result + '.gz') -@pytest.mark.parametrize("pair,expected_result", [ - ("ETH/BTC", 'freqtrade/hello/world/ETH_BTC-trades.json'), - ("Fabric Token/ETH", 'freqtrade/hello/world/Fabric_Token_ETH-trades.json'), - ("ETHH20", 'freqtrade/hello/world/ETHH20-trades.json'), - (".XBTBON2H", 'freqtrade/hello/world/_XBTBON2H-trades.json'), - ("ETHUSD.d", 'freqtrade/hello/world/ETHUSD_d-trades.json'), - ("ACC_OLD_BTC", 'freqtrade/hello/world/ACC_OLD_BTC-trades.json'), +@pytest.mark.parametrize("pair,trading_mode,expected_result", [ + ("ETH/BTC", '', 'freqtrade/hello/world/ETH_BTC-trades.json'), + ("ETH/USDT:USDT", 'futures', 'freqtrade/hello/world/futures/ETH_USDT_USDT-trades.json'), + ("Fabric Token/ETH", '', 'freqtrade/hello/world/Fabric_Token_ETH-trades.json'), + ("ETHH20", '', 'freqtrade/hello/world/ETHH20-trades.json'), + (".XBTBON2H", '', 'freqtrade/hello/world/_XBTBON2H-trades.json'), + ("ETHUSD.d", '', 'freqtrade/hello/world/ETHUSD_d-trades.json'), + ("ACC_OLD_BTC", '', 'freqtrade/hello/world/ACC_OLD_BTC-trades.json'), ]) -def test_json_pair_trades_filename(pair, expected_result): - fn = JsonDataHandler._pair_trades_filename(Path('freqtrade/hello/world'), pair) +def test_json_pair_trades_filename(pair, trading_mode, expected_result): + fn = JsonDataHandler._pair_trades_filename(Path('freqtrade/hello/world'), pair, trading_mode) assert isinstance(fn, Path) assert fn == Path(expected_result) - fn = JsonGzDataHandler._pair_trades_filename(Path('freqtrade/hello/world'), pair) + fn = JsonGzDataHandler._pair_trades_filename(Path('freqtrade/hello/world'), pair, trading_mode) assert isinstance(fn, Path) assert fn == Path(expected_result + '.gz') @@ -325,7 +326,7 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] ] json_dump_mock = mocker.patch( - 'freqtrade.data.history.featherdatahandler.FeatherDataHandler.ohlcv_store', + 'freqtrade.data.history.datahandlers.featherdatahandler.FeatherDataHandler.ohlcv_store', return_value=None) mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) @@ -559,7 +560,8 @@ def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, tes unavailable_pairs = refresh_backtest_trades_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC", "XRP/ETH"], datadir=testdatadir, - timerange=timerange, erase=True + timerange=timerange, erase=True, + trading_mode=TradingMode.SPOT, ) assert dl_mock.call_count == 2 @@ -584,7 +586,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad assert not file1.is_file() assert _download_trades_history(data_handler=data_handler, exchange=exchange, - pair='ETH/BTC') + pair='ETH/BTC', trading_mode=TradingMode.SPOT) assert log_has("Current Amount of trades: 0", caplog) assert log_has("New Amount of trades: 6", caplog) assert ght_mock.call_count == 1 @@ -597,8 +599,9 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad since_time = int(trades_history[-3][0] // 1000) since_time2 = int(trades_history[-1][0] // 1000) timerange = TimeRange('date', None, since_time, 0) - assert _download_trades_history(data_handler=data_handler, exchange=exchange, - pair='ETH/BTC', timerange=timerange) + assert _download_trades_history( + data_handler=data_handler, exchange=exchange, pair='ETH/BTC', + timerange=timerange, trading_mode=TradingMode.SPOT) assert ght_mock.call_count == 1 # Check this in seconds - since we had to convert to seconds above too. @@ -611,7 +614,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad caplog.clear() assert not _download_trades_history(data_handler=data_handler, exchange=exchange, - pair='ETH/BTC') + pair='ETH/BTC', trading_mode=TradingMode.SPOT) assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) file2 = tmp_path / 'XRP_ETH-trades.json.gz' @@ -623,8 +626,9 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad since_time = int(trades_history[0][0] // 1000) - 500 timerange = TimeRange('date', None, since_time, 0) - assert _download_trades_history(data_handler=data_handler, exchange=exchange, - pair='XRP/ETH', timerange=timerange) + assert _download_trades_history( + data_handler=data_handler, exchange=exchange, pair='XRP/ETH', + timerange=timerange, trading_mode=TradingMode.SPOT) assert ght_mock.call_count == 1 diff --git a/tests/data/test_trade_converter_kraken.py b/tests/data/test_trade_converter_kraken.py index 91de303fb..cc5721030 100644 --- a/tests/data/test_trade_converter_kraken.py +++ b/tests/data/test_trade_converter_kraken.py @@ -5,7 +5,8 @@ from unittest.mock import PropertyMock import pytest from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv -from freqtrade.data.history.idatahandler import get_datahandler +from freqtrade.data.history import get_datahandler +from freqtrade.enums import TradingMode from freqtrade.exceptions import OperationalException from tests.conftest import EXMS, log_has, log_has_re, patch_exchange @@ -40,7 +41,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmp_path, caplog, default_co assert dstfile.is_file() dh = get_datahandler(tmp_path, 'feather') - trades = dh.trades_load('BCH_EUR') + trades = dh.trades_load('BCH_EUR', TradingMode.SPOT) assert len(trades) == 340 assert trades['date'].min().to_pydatetime() == datetime(2023, 1, 1, 0, 3, 56, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5c4879a32..691ee0b87 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -7,11 +7,12 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch import ccxt import pytest +from numpy import NaN from pandas import DataFrame from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode -from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, - InsufficientFundsError, InvalidOrderException, +from freqtrade.exceptions import (ConfigurationError, DDosProtection, DependencyException, + ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.exchange import (Binance, Bybit, Exchange, Kraken, market_is_active, timeframe_to_prev_date) @@ -594,7 +595,7 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog): mocker.patch(f'{EXMS}.validate_pairs') mocker.patch(f'{EXMS}.validate_timeframes') mocker.patch(f'{EXMS}._load_async_markets') - with pytest.raises(OperationalException, + with pytest.raises(ConfigurationError, match=r'XRP is not available as stake on .*' 'Available currencies are: BTC, ETH, USDT'): Exchange(default_conf) @@ -799,12 +800,12 @@ def test_validate_timeframes_failed(default_conf, mocker): mocker.patch(f'{EXMS}.validate_pairs') mocker.patch(f'{EXMS}.validate_stakecurrency') mocker.patch(f'{EXMS}.validate_pricing') - with pytest.raises(OperationalException, + with pytest.raises(ConfigurationError, match=r"Invalid timeframe '3m'. This exchange supports.*"): Exchange(default_conf) default_conf["timeframe"] = "15s" - with pytest.raises(OperationalException, + with pytest.raises(ConfigurationError, match=r"Timeframes < 1m are currently not supported by Freqtrade."): Exchange(default_conf) @@ -1065,6 +1066,9 @@ def test_exchange_has(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock) assert not exchange.exchange_has("deadbeef") + exchange._ft_has['exchange_has_overrides'] = {'deadbeef': True} + assert exchange.exchange_has("deadbeef") + @pytest.mark.parametrize("side,leverage", [ ("buy", 1), @@ -4203,6 +4207,7 @@ def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value, (10, 0.0001, 2.0, 1.0, 0.002, 0.002), (10, 0.0002, 2.0, 0.01, 0.004, 0.00004), (10, 0.0002, 2.5, None, 0.005, None), + (10, 0.0002, NaN, None, 0.0, None), ]) def test_calculate_funding_fees( default_conf, @@ -4312,8 +4317,8 @@ def test_combine_funding_and_mark( assert len(df) == 1 # Empty funding rates - funding_rates = DataFrame([], columns=['date', 'open']) - df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate) + funding_rates2 = DataFrame([], columns=['date', 'open']) + df = exchange.combine_funding_and_mark(funding_rates2, mark_rates, futures_funding_rate) if futures_funding_rate is not None: assert len(df) == 3 assert df.iloc[0]['open_fund'] == futures_funding_rate @@ -4322,6 +4327,12 @@ def test_combine_funding_and_mark( else: assert len(df) == 0 + # Empty mark candles + mark_candles = DataFrame([], columns=['date', 'open']) + df = exchange.combine_funding_and_mark(funding_rates, mark_candles, futures_funding_rate) + + assert len(df) == 0 + @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ ('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0), diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 81d72d92a..4a1976d9d 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -25,6 +25,11 @@ def is_mac() -> bool: return "Darwin" in machine +def is_arm() -> bool: + machine = platform.machine() + return "arm" in machine or "aarch64" in machine + + @pytest.fixture(autouse=True) def patch_torch_initlogs(mocker) -> None: @@ -36,7 +41,7 @@ def patch_torch_initlogs(mocker) -> None: module_name = 'torch' mocked_module = types.ModuleType(module_name) sys.modules[module_name] = mocked_module - elif not is_py12(): + else: mocker.patch("torch._logging._init_logs") diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 58648d97f..cd07b913b 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -1,5 +1,4 @@ import logging -import platform import shutil from pathlib import Path from unittest.mock import MagicMock @@ -15,25 +14,17 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager 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, is_py12, make_rl_config, +from tests.freqai.conftest import (get_patched_freqai_strategy, is_arm, is_mac, make_rl_config, mock_pytorch_mlp_model_training_parameters) -def is_arm() -> bool: - machine = platform.machine() - return "arm" in machine or "aarch64" in machine - - def can_run_model(model: str) -> None: is_pytorch_model = 'Reinforcement' in model or 'PyTorch' in model - if is_py12() and ("Catboost" in model or is_pytorch_model): - pytest.skip("Model not supported on python 3.12 yet.") - if is_arm() and "Catboost" in model: pytest.skip("CatBoost is not supported on ARM.") - if is_pytorch_model and is_mac() and not is_arm(): + if is_pytorch_model and is_mac(): pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.") @@ -243,7 +234,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog): can_run_model(model) test_tb = True - if is_mac(): + if is_mac() and not is_arm(): test_tb = False freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) @@ -527,8 +518,6 @@ 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_py12(): - pytest.skip("Reinforcement learning currently not available on python 3.12.") freqai_conf.update({"freqaimodel": "ReinforcementLearner"}) freqai_conf.update({"timerange": "20180110-20180130"}) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 1891c2332..0106e1b77 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -1233,6 +1233,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca order_id=order_id, )) + freqtrade.strategy.order_filled = MagicMock(return_value=None) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) caplog.clear() @@ -1243,6 +1244,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca caplog.clear() assert not trade.has_open_orders assert trade.amount == order['amount'] + assert freqtrade.strategy.order_filled.call_count == 1 mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.01) assert trade.amount == 30.0 @@ -1260,11 +1262,13 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca limit_buy_order_usdt_new['filled'] = 0.0 limit_buy_order_usdt_new['status'] = 'canceled' + freqtrade.strategy.order_filled = MagicMock(return_value=None) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError) 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 + assert freqtrade.strategy.order_filled.call_count == 0 @pytest.mark.parametrize("is_short", [False, True]) @@ -5460,9 +5464,10 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca assert freqtrade.strategy.adjust_trade_position.call_count == 1 caplog.clear() - freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-10, 'partial_exit_c')) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-0.0005, 'partial_exit_c')) freqtrade.process_open_trade_positions() assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog) assert freqtrade.strategy.adjust_trade_position.call_count == 1 trade = Trade.get_trades(trade_filter=[Trade.id == 5]).first() assert trade.orders[-1].ft_order_tag == 'partial_exit_c' + assert trade.is_open diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 522693131..3384ae49f 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -636,12 +636,12 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera assert len(trade.orders) == 2 assert trade.orders[-1].ft_order_side == 'sell' assert trade.orders[-1].ft_order_tag == 'PES' - assert pytest.approx(trade.stake_amount) == 40.198 - assert pytest.approx(trade.amount) == 20.099 * leverage + assert pytest.approx(trade.stake_amount) == 40 + assert pytest.approx(trade.amount) == 20 * leverage assert trade.open_rate == 2.0 assert trade.is_open assert trade.realized_profit > 0.098 * leverage - expected_profit = starting_amount - 40.1980 + trade.realized_profit + expected_profit = starting_amount - 40 + trade.realized_profit assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit if spot: @@ -667,14 +667,14 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera # Amount exactly comes out as exactly 0 freqtrade.strategy.adjust_trade_position = MagicMock( - return_value=-(trade.amount / trade.leverage * 2.02)) + return_value=-trade.stake_amount) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 3 assert trade.orders[-1].ft_order_side == 'sell' - assert pytest.approx(trade.stake_amount) == 40.198 + assert pytest.approx(trade.stake_amount) == 40 assert trade.is_open is False # use amount that would trunc to 0.0 once selling @@ -684,7 +684,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera trade = Trade.get_trades().first() assert len(trade.orders) == 3 assert trade.orders[-1].ft_order_side == 'sell' - assert pytest.approx(trade.stake_amount) == 40.198 + assert pytest.approx(trade.stake_amount) == 40 assert trade.is_open is False assert log_has_re('Amount to exit is 0.0 due to exchange limits - not exiting.', caplog) expected_profit = starting_amount - 60 + trade.realized_profit diff --git a/tests/freqtradebot/test_stoploss_on_exchange.py b/tests/freqtradebot/test_stoploss_on_exchange.py index 325fe549f..04a04ea9f 100644 --- a/tests/freqtradebot/test_stoploss_on_exchange.py +++ b/tests/freqtradebot/test_stoploss_on_exchange.py @@ -146,10 +146,12 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ 'amount': enter_order['amount'], }) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) + freqtrade.strategy.order_filled = MagicMock(return_value=None) 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 len(trade.open_sl_orders) == 0 assert trade.is_open is False + assert freqtrade.strategy.order_filled.call_count == 1 caplog.clear() mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 603fcc310..8fc42b474 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -698,6 +698,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange) processed = backtesting.strategy.advise_all_indicators(data) + backtesting.strategy.order_filled = MagicMock() min_date, max_date = get_timerange(processed) result = backtesting.backtest( @@ -760,6 +761,8 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: pd.testing.assert_frame_equal(results, expected) assert 'orders' in results.columns data_pair = processed[pair] + # Called once per order + assert backtesting.strategy.order_filled.call_count == 4 for _, t in results.iterrows(): assert len(t['orders']) == 2 ln = data_pair.loc[data_pair["date"] == t["open_date"]] diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 0f190f3f5..57605d038 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -59,13 +59,13 @@ def test_text_table_bt_results(): ) result_str = ( - '| Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | ' + '| Pair | Entries | Avg Profit % | Tot Profit BTC | ' 'Tot Profit % | Avg Duration | Win Draw Loss Win% |\n' - '|---------+-----------+----------------+----------------+------------------+' + '|---------+-----------+----------------+------------------+' '----------------+----------------+-------------------------|\n' - '| ETH/BTC | 3 | 8.33 | 25.00 | 0.50000000 | ' + '| ETH/BTC | 3 | 8.33 | 0.50000000 | ' '12.50 | 0:20:00 | 2 0 1 66.7 |\n' - '| TOTAL | 3 | 8.33 | 25.00 | 0.50000000 | ' + '| TOTAL | 3 | 8.33 | 0.50000000 | ' '12.50 | 0:20:00 | 2 0 1 66.7 |' ) @@ -392,13 +392,13 @@ def test_text_table_exit_reason(): ) result_str = ( - '| Exit Reason | Exits | Win Draws Loss Win% | Avg Profit % | Cum Profit % |' + '| Exit Reason | Exits | Win Draws Loss Win% | Avg Profit % |' ' Tot Profit BTC | Tot Profit % |\n' - '|---------------+---------+--------------------------+----------------+----------------+' + '|---------------+---------+--------------------------+----------------+' '------------------+----------------|\n' - '| roi | 2 | 2 0 0 100 | 15 | 30 |' + '| roi | 2 | 2 0 0 100 | 15 |' ' 0.6 | 15 |\n' - '| stop_loss | 1 | 0 0 1 0 | -10 | -10 |' + '| stop_loss | 1 | 0 0 1 0 | -10 |' ' -0.2 | -5 |' ) @@ -450,13 +450,13 @@ def test_text_table_strategy(testdatadir): bt_res_data_comparison = bt_res_data.pop('strategy_comparison') result_str = ( - '| Strategy | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC |' + '| Strategy | Entries | Avg Profit % | Tot Profit BTC |' ' Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n' - '|----------------+-----------+----------------+----------------+------------------+' + '|----------------+-----------+----------------+------------------+' '----------------+----------------+-------------------------+-----------------------|\n' - '| StrategyTestV2 | 179 | 0.08 | 14.39 | 0.02608550 |' + '| StrategyTestV2 | 179 | 0.08 | 0.02608550 |' ' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n' - '| TestStrategy | 179 | 0.08 | 14.39 | 0.02608550 |' + '| TestStrategy | 179 | 0.08 | 0.02608550 |' ' 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |' ) @@ -498,7 +498,7 @@ def test__get_resample_from_period(): assert _get_resample_from_period('day') == '1d' assert _get_resample_from_period('week') == '1W-MON' - assert _get_resample_from_period('month') == '1M' + assert _get_resample_from_period('month') == '1ME' with pytest.raises(ValueError, match=r"Period noooo is not supported."): _get_resample_from_period('noooo') diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 0e0e70ee8..18f28da2b 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -2099,6 +2099,7 @@ def test_Trade_object_idem(): 'get_mix_tag_performance', 'get_trading_volume', 'validate_string_len', + 'custom_data' ) EXCLUDES2 = ('trades', 'trades_open', 'bt_trades_open_pp', 'bt_open_open_trade_count', 'total_profit', 'from_json',) diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py new file mode 100644 index 000000000..15241aa93 --- /dev/null +++ b/tests/persistence/test_trade_custom_data.py @@ -0,0 +1,160 @@ +from copy import deepcopy +from unittest.mock import MagicMock + +import pytest + +from freqtrade.data.history.history_utils import get_timerange +from freqtrade.optimize.backtesting import Backtesting +from freqtrade.persistence import Trade, disable_database_use, enable_database_use +from freqtrade.persistence.custom_data import CustomDataWrapper +from tests.conftest import (EXMS, create_mock_trades_usdt, generate_test_data, + get_patched_freqtradebot, patch_exchange) + + +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize("use_db", [True, False]) +def test_trade_custom_data(fee, use_db): + if not use_db: + disable_database_use('5m') + Trade.reset_trades() + CustomDataWrapper.reset_custom_data() + + create_mock_trades_usdt(fee, use_db=use_db) + + trade1 = Trade.get_trades_proxy()[0] + if not use_db: + trade1.id = 1 + + assert trade1.get_all_custom_data() == [] + trade1.set_custom_data('test_str', 'test_value') + trade1.set_custom_data('test_int', 1) + trade1.set_custom_data('test_float', 1.55) + trade1.set_custom_data('test_bool', True) + trade1.set_custom_data('test_dict', {'test': 'dict'}) + + assert len(trade1.get_all_custom_data()) == 5 + assert trade1.get_custom_data('test_str') == 'test_value' + trade1.set_custom_data('test_str', 'test_value_updated') + assert trade1.get_custom_data('test_str') == 'test_value_updated' + + assert trade1.get_custom_data('test_int') == 1 + assert isinstance(trade1.get_custom_data('test_int'), int) + + assert trade1.get_custom_data('test_float') == 1.55 + assert isinstance(trade1.get_custom_data('test_float'), float) + + assert trade1.get_custom_data('test_bool') is True + assert isinstance(trade1.get_custom_data('test_bool'), bool) + + assert trade1.get_custom_data('test_dict') == {'test': 'dict'} + assert isinstance(trade1.get_custom_data('test_dict'), dict) + if not use_db: + enable_database_use() + + +def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): + + mocker.patch(f'{EXMS}.get_rate', return_value=0.50) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=None) + default_conf_usdt["minimal_roi"] = {"0": 100} + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + create_mock_trades_usdt(fee) + + trade1 = Trade.get_trades_proxy(pair='ADA/USDT')[0] + trade1.set_custom_data('test_str', 'test_value') + trade1.set_custom_data('test_int', 1) + + def custom_exit(pair, trade, **kwargs): + + if pair == 'ADA/USDT': + custom_val = trade.get_custom_data('test_str') + custom_val_i = trade.get_custom_data('test_int') + + return f"{custom_val}_{custom_val_i}" + + freqtrade.strategy.custom_exit = custom_exit + ff_spy = mocker.spy(freqtrade.strategy, 'custom_exit') + trades = Trade.get_open_trades() + freqtrade.exit_positions(trades) + Trade.commit() + + trade_after = Trade.get_trades_proxy(pair='ADA/USDT')[0] + assert trade_after.get_custom_data('test_str') == 'test_value' + assert trade_after.get_custom_data('test_int') == 1 + # 2 open pairs eligible for exit + assert ff_spy.call_count == 2 + + assert trade_after.exit_reason == 'test_value_1' + + +def test_trade_custom_data_strategy_backtest_compat(mocker, default_conf_usdt, fee): + + 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) + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.1, 0.1)) + mocker.patch('freqtrade.optimize.backtesting.Backtesting._run_funding_fees') + + patch_exchange(mocker) + default_conf_usdt.update({ + "stake_amount": 100.0, + "max_open_trades": 2, + "dry_run_wallet": 1000.0, + "strategy": "StrategyTestV3", + "trading_mode": "futures", + "margin_mode": "isolated", + "stoploss": -2, + "minimal_roi": {"0": 100}, + }) + default_conf_usdt['pairlists'] = [{'method': 'StaticPairList', 'allow_inactive': True}] + backtesting = Backtesting(default_conf_usdt) + + df = generate_test_data(default_conf_usdt['timeframe'], 100, '2022-01-01 00:00:00+00:00') + + pair_exp = 'XRP/USDT:USDT' + + def custom_exit(pair, trade, **kwargs): + custom_val = trade.get_custom_data('test_str') + custom_val_i = trade.get_custom_data('test_int', 0) + + if pair == pair_exp: + trade.set_custom_data('test_str', 'test_value') + trade.set_custom_data('test_int', custom_val_i + 1) + + if custom_val_i >= 2: + return f"{custom_val}_{custom_val_i}" + + backtesting._set_strategy(backtesting.strategylist[0]) + processed = backtesting.strategy.advise_all_indicators({ + pair_exp: df, + 'BTC/USDT:USDT': df, + }) + + def fun(dataframe, *args, **kwargs): + dataframe.loc[dataframe.index == 50, 'enter_long'] = 1 + return dataframe + + backtesting.strategy.advise_entry = fun + backtesting.strategy.leverage = MagicMock(return_value=1) + backtesting.strategy.custom_exit = custom_exit + ff_spy = mocker.spy(backtesting.strategy, 'custom_exit') + + 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 + assert len(results) == 2 + assert results['pair'][0] == pair_exp + assert results['pair'][1] == 'BTC/USDT:USDT' + assert results['exit_reason'][0] == 'test_value_2' + assert results['exit_reason'][1] == 'exit_signal' + + assert ff_spy.call_count == 7 + Backtesting.cleanup() diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index d125f8896..39f48f454 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -19,7 +19,7 @@ from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver from freqtrade.util.datetime_helpers import dt_now -from tests.conftest import (EXMS, create_mock_trades_usdt, get_patched_exchange, +from tests.conftest import (EXMS, create_mock_trades_usdt, generate_test_data, get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re, num_log_has) @@ -406,6 +406,14 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "min_value": 1250}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + # HOT, XRP and FUEL whitelisted because they are below 1300 quoteVolume. + ([{"method": "VolumePairList", "number_assets": 5, + "sort_key": "quoteVolume", "max_value": 1300}], + "BTC", ['XRP/BTC', 'HOT/BTC', 'FUEL/BTC']), + # HOT, XRP whitelisted because they are between 100 and 1300 quoteVolume. + ([{"method": "VolumePairList", "number_assets": 5, + "sort_key": "quoteVolume", "min_value": 100, "max_value": 1300}], + "BTC", ['XRP/BTC', 'HOT/BTC']), # StaticPairlist only ([{"method": "StaticPairList"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), @@ -748,6 +756,104 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: assert log_has("PerformanceFilter is not available in this mode.", caplog) +def test_VolatilityFilter_error(mocker, whitelist_conf) -> None: + volatility_filter = {"method": "VolatilityFilter", "lookback_days": -1} + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] + + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + exchange_mock = MagicMock() + exchange_mock.ohlcv_candle_limit = MagicMock(return_value=1000) + + with pytest.raises(OperationalException, + match=r"VolatilityFilter requires lookback_days to be >= 1*"): + PairListManager(exchange_mock, whitelist_conf, MagicMock()) + + volatility_filter = {"method": "VolatilityFilter", "lookback_days": 2000} + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] + with pytest.raises(OperationalException, + match=r"VolatilityFilter requires lookback_days to not exceed exchange max"): + PairListManager(exchange_mock, whitelist_conf, MagicMock()) + + volatility_filter = {"method": "VolatilityFilter", "sort_direction": "Random"} + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] + with pytest.raises(OperationalException, + match=r"VolatilityFilter requires sort_direction to be either " + r"None .*'asc'.*'desc'"): + PairListManager(exchange_mock, whitelist_conf, MagicMock()) + + +@pytest.mark.parametrize('pairlist,expected_pairlist', [ + ({"method": "VolatilityFilter", "sort_direction": "asc"}, + ['XRP/BTC', 'ETH/BTC', 'LTC/BTC', 'TKN/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "desc"}, + ['TKN/BTC', 'LTC/BTC', 'ETH/BTC', 'XRP/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "desc", 'min_volatility': 0.4}, + ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "asc", 'min_volatility': 0.4}, + ['ETH/BTC', 'LTC/BTC', 'TKN/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "desc", 'max_volatility': 0.5}, + ['LTC/BTC', 'ETH/BTC', 'XRP/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "asc", 'max_volatility': 0.5}, + ['XRP/BTC', 'ETH/BTC', 'LTC/BTC']), + ({"method": "RangeStabilityFilter", "sort_direction": "asc"}, + ['ETH/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), + ({"method": "RangeStabilityFilter", "sort_direction": "desc"}, + ['TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'ETH/BTC']), + ({"method": "RangeStabilityFilter", "sort_direction": "asc", 'min_rate_of_change': 0.4}, + ['XRP/BTC', 'LTC/BTC', 'TKN/BTC']), + ({"method": "RangeStabilityFilter", "sort_direction": "desc", 'min_rate_of_change': 0.4}, + ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']), +]) +def test_VolatilityFilter_RangeStabilityFilter_sort( + mocker, whitelist_conf, tickers, time_machine, pairlist, expected_pairlist) -> None: + whitelist_conf['pairlists'] = [ + {'method': 'VolumePairList', 'number_assets': 10}, + pairlist + ] + + df1 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=42) + df2 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=2) + df3 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=3) + df4 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=4) + df5 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=5) + df6 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=6) + + assert not df1.equals(df2) + time_machine.move_to('2022-01-15 00:00:00+00:00') + + ohlcv_data = { + ('ETH/BTC', '1d', CandleType.SPOT): df1, + ('TKN/BTC', '1d', CandleType.SPOT): df2, + ('LTC/BTC', '1d', CandleType.SPOT): df3, + ('XRP/BTC', '1d', CandleType.SPOT): df4, + ('HOT/BTC', '1d', CandleType.SPOT): df5, + ('BLK/BTC', '1d', CandleType.SPOT): df6, + + } + ohlcv_mock = MagicMock(return_value=ohlcv_data) + mocker.patch.multiple( + EXMS, + exchange_has=MagicMock(return_value=True), + refresh_latest_ohlcv=ohlcv_mock, + get_tickers=tickers + + ) + + exchange = get_patched_exchange(mocker, whitelist_conf) + exchange.ohlcv_candle_limit = MagicMock(return_value=1000) + plm = PairListManager(exchange, whitelist_conf, MagicMock()) + + assert exchange.ohlcv_candle_limit.call_count == 2 + plm.refresh_pairlist() + assert ohlcv_mock.call_count == 1 + assert exchange.ohlcv_candle_limit.call_count == 2 + assert plm.whitelist == expected_pairlist + + plm.refresh_pairlist() + assert exchange.ohlcv_candle_limit.call_count == 2 + assert ohlcv_mock.call_count == 1 + + def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: whitelist_conf['pairlists'] = [ {"method": "StaticPairList"}, @@ -1095,6 +1201,13 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): match='RangeStabilityFilter requires lookback_days to be >= 1'): get_patched_freqtradebot(mocker, default_conf) + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'RangeStabilityFilter', 'sort_direction': 'something'}] + + with pytest.raises(OperationalException, + match='RangeStabilityFilter requires sort_direction to be either None.*'): + get_patched_freqtradebot(mocker, default_conf) + @pytest.mark.parametrize('min_rate_of_change,max_rate_of_change,expected_length', [ (0.01, 0.99, 5), diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 85b105892..0bf39fab7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -10,6 +10,7 @@ from freqtrade.edge import PairInfo from freqtrade.enums import SignalDirection, State, TradingMode from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Order, Trade +from freqtrade.persistence.key_value_store import set_startup_time from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -223,8 +224,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert "Pair" in headers assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] - assert '0.00' == result[0][3] - assert isnan(fiat_profit_sum) + assert '0.00 (0.00)' == result[0][3] + assert '0.00' == f'{fiat_profit_sum:.2f}' mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) freqtradebot.process() @@ -234,8 +235,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert "Pair" in headers assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] - assert '-0.41%' == result[0][3] - assert isnan(fiat_profit_sum) + assert '-0.41% (-0.00)' == result[0][3] + assert '-0.00' == f'{fiat_profit_sum:.2f}' # Test with fiat convert rpc._fiat_converter = CryptoToFiatConverter() @@ -490,12 +491,12 @@ def test_rpc_balance_handle_error(default_conf, mocker): rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) -def test_rpc_balance_handle(default_conf, mocker, tickers): +def test_rpc_balance_handle(default_conf_usdt, mocker, tickers): mock_balance = { 'BTC': { - 'free': 10.0, - 'total': 12.0, - 'used': 2.0, + 'free': 0.01, + 'total': 0.012, + 'used': 0.002, }, 'ETH': { 'free': 1.0, @@ -503,8 +504,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'used': 4.0, }, 'USDT': { - 'free': 5.0, - 'total': 10.0, + 'free': 50.0, + 'total': 100.0, 'used': 5.0, } } @@ -518,10 +519,10 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): "maintenanceMargin": 0.0, "maintenanceMarginPercentage": 0.005, "entryPrice": 0.0, - "notional": 100.0, + "notional": 10.0, "leverage": 5.0, "unrealizedPnl": 0.0, - "contracts": 100.0, + "contracts": 1.0, "contractSize": 1, "marginRatio": None, "liquidationPrice": 0.0, @@ -535,9 +536,9 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): mocker.patch.multiple( 'freqtrade.rpc.fiat_convert.CoinGeckoAPI', - get_price=MagicMock(return_value={'bitcoin': {'usd': 15000.0}}), + get_price=MagicMock(return_value={'bitcoin': {'usd': 1.2}}), ) - mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.2) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( EXMS, @@ -548,86 +549,86 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): get_valid_pair_combination=MagicMock( side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") ) - default_conf['dry_run'] = False - default_conf['trading_mode'] = 'futures' - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + default_conf_usdt['dry_run'] = False + default_conf_usdt['trading_mode'] = 'futures' + freqtradebot = get_patched_freqtradebot(mocker, default_conf_usdt) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() - result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) - assert pytest.approx(result['total']) == 30.30909624 - assert pytest.approx(result['value']) == 454636.44360691 + result = rpc._rpc_balance( + default_conf_usdt['stake_currency'], default_conf_usdt['fiat_display_currency']) + + assert pytest.approx(result['total']) == 2824.83464 + assert pytest.approx(result['value']) == 2824.83464 * 1.2 assert tickers.call_count == 1 assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ { 'currency': 'BTC', - '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, + 'free': 0.01, + 'balance': 0.012, + 'used': 0.002, + 'bot_owned': 0, + 'est_stake': 103.78464, + 'est_stake_bot': 0, + 'stake': 'USDT', 'side': 'long', - 'is_bot_managed': True, + 'leverage': 1, + 'position': 0, + 'is_bot_managed': False, + 'is_position': False }, { + 'currency': 'ETH', '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': 2651.05, 'est_stake_bot': 0, - 'used': 5.0, - 'stake': 'BTC', - 'is_position': False, - 'leverage': 1.0, - 'position': 0.0, + 'stake': 'USDT', 'side': 'long', + 'leverage': 1, + 'position': 0, 'is_bot_managed': False, + 'is_position': False + }, + { + 'currency': 'USDT', + 'free': 50.0, + 'balance': 100.0, + 'used': 5.0, + 'bot_owned': 49.5, + 'est_stake': 50.0, + 'est_stake_bot': 49.5, + 'stake': 'USDT', + 'side': 'long', + 'leverage': 1, + 'position': 0, + 'is_bot_managed': True, + 'is_position': False }, { - 'free': 0.0, - 'balance': 0.0, 'currency': 'ETH/USDT:USDT', + 'free': 0, + 'balance': 0, + 'used': 0, + 'position': 10.0, 'est_stake': 20, 'est_stake_bot': 20, - 'used': 0, - 'stake': 'BTC', - 'is_position': True, + 'stake': 'USDT', 'leverage': 5.0, - 'position': 1000.0, 'side': 'short', 'is_bot_managed': True, + 'is_position': 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) + assert pytest.approx(result['total_bot']) == 69.5 + assert pytest.approx(result['total']) == 2824.83464 # ETH stake is missing. + assert result['starting_capital'] == 50 + assert result['starting_capital_ratio'] == pytest.approx(0.3899999) def test_rpc_start(mocker, default_conf) -> None: @@ -1298,6 +1299,7 @@ def test_rpc_health(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) + set_startup_time() rpc = RPC(freqtradebot) result = rpc.health() assert result['last_process'] is None diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7b1347fd6..3bd372b19 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -150,7 +150,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " "['bl_delete', 'blacklist_delete'], " "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], " - "['order']]") + "['order'], ['list_custom_data']]") assert log_has(message_str, caplog) @@ -2657,3 +2657,49 @@ async def test_change_market_direction(default_conf, mocker, update) -> None: context.args = ["invalid"] await telegram._changemarketdir(update, context) assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG + + +async def test_telegram_list_custom_data(default_conf_usdt, update, ticker, fee, mocker) -> None: + + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker, + get_fee=fee, + ) + telegram, _freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) + + # Create some test data + create_mock_trades_usdt(fee) + # No trade id + context = MagicMock() + await telegram._list_custom_data(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Trade-id not set.' in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + + # + context.args = ['1'] + await telegram._list_custom_data(update=update, context=context) + assert msg_mock.call_count == 1 + assert ( + "Didn't find any custom-data entries for Trade ID: `1`" in msg_mock.call_args_list[0][0][0] + ) + msg_mock.reset_mock() + + # Add some custom data + trade1 = Trade.get_trades_proxy()[0] + trade1.set_custom_data('test_int', 1) + trade1.set_custom_data('test_dict', {'test': 'dict'}) + Trade.commit() + context.args = [f"{trade1.id}"] + await telegram._list_custom_data(update=update, context=context) + assert msg_mock.call_count == 3 + assert "Found custom-data entries: " in msg_mock.call_args_list[0][0][0] + assert ( + "*Key:* `test_int`\n*ID:* `1`\n*Trade ID:* `1`\n*Type:* `int`\n" + "*Value:* `1`\n*Create Date:*") in msg_mock.call_args_list[1][0][0] + assert ( + '*Key:* `test_dict`\n*ID:* `2`\n*Trade ID:* `1`\n*Type:* `dict`\n' + '*Value:* `{"test": "dict"}`\n*Create Date:* `') in msg_mock.call_args_list[2][0][0] + + msg_mock.reset_mock() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 790f5d255..dbff49b57 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -795,9 +795,6 @@ def test_strategy_safe_wrapper_error(caplog, error): def failing_method(): raise error('This is an error.') - def working_method(argumentpassedin): - return argumentpassedin - with pytest.raises(StrategyError, match=r'This is an error.'): strategy_safe_wrapper(failing_method, message='DeadBeef')() @@ -1022,22 +1019,22 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): @pytest.mark.parametrize('function,raises', [ - ('populate_entry_trend', True), + ('populate_entry_trend', False), ('advise_entry', False), - ('populate_exit_trend', True), + ('populate_exit_trend', False), ('advise_exit', False), ]) -def test_pandas_warning_direct(ohlcv_history, function, raises): +def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn): df = _STRATEGY.populate_indicators(ohlcv_history, {'pair': 'ETH/BTC'}) if raises: - with pytest.warns(FutureWarning): - # Test for Future warning - # FutureWarning: Setting an item of incompatible dtype is - # deprecated and will raise in a future error of pandas - # https://github.com/pandas-dev/pandas/issues/56503 - getattr(_STRATEGY, function)(df, {'pair': 'ETH/BTC'}) + assert len(recwarn) == 1 + # https://github.com/pandas-dev/pandas/issues/56503 + # Fixed in 2.2.x + getattr(_STRATEGY, function)(df, {'pair': 'ETH/BTC'}) else: + assert len(recwarn) == 0 + getattr(_STRATEGY, function)(df, {'pair': 'ETH/BTC'}) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 71585cac3..124258c04 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -10,6 +10,7 @@ from jsonschema import ValidationError from freqtrade.commands import Arguments from freqtrade.configuration import Configuration, validate_config_consistency +from freqtrade.configuration.config_secrets import sanitize_config from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import (check_conflicting_settings, process_deprecated_setting, @@ -1426,7 +1427,7 @@ def test_flat_vars_to_nested_dict(caplog): assert not log_has("Loading variable 'NOT_RELEVANT'", caplog) -def test_setup_hyperopt_freqai(mocker, default_conf, caplog) -> None: +def test_setup_hyperopt_freqai(mocker, default_conf) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', @@ -1459,7 +1460,7 @@ def test_setup_hyperopt_freqai(mocker, default_conf, caplog) -> None: validate_config_consistency(config) -def test_setup_freqai_backtesting(mocker, default_conf, caplog) -> None: +def test_setup_freqai_backtesting(mocker, default_conf) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch( 'freqtrade.configuration.configuration.create_datadir', @@ -1506,3 +1507,17 @@ def test_setup_freqai_backtesting(mocker, default_conf, caplog) -> None: OperationalException, match=r".* pass --timerange if you intend to use FreqAI .*" ): validate_config_consistency(conf) + + +def test_sanitize_config(default_conf_usdt): + assert default_conf_usdt['exchange']['key'] != 'REDACTED' + res = sanitize_config(default_conf_usdt) + # Didn't modify original dict + assert default_conf_usdt['exchange']['key'] != 'REDACTED' + + assert res['exchange']['key'] == 'REDACTED' + assert res['exchange']['secret'] == 'REDACTED' + + res = sanitize_config(default_conf_usdt, show_sensitive=True) + assert res['exchange']['key'] == default_conf_usdt['exchange']['key'] + assert res['exchange']['secret'] == default_conf_usdt['exchange']['secret'] diff --git a/tests/test_main.py b/tests/test_main.py index 442e6e1d7..4b28c094f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -8,7 +8,7 @@ import pytest from freqtrade.commands import Arguments from freqtrade.enums import State -from freqtrade.exceptions import FreqtradeException, OperationalException +from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.worker import Worker @@ -141,6 +141,22 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None: assert log_has_re(r'SIGINT.*', caplog) +def test_main_ConfigurationError(mocker, default_conf, caplog) -> None: + patch_exchange(mocker) + mocker.patch( + 'freqtrade.commands.list_commands.list_available_exchanges', + MagicMock(side_effect=ConfigurationError('Oh snap!')) + ) + patched_configuration_load_config_file(mocker, default_conf) + + args = ['list-exchanges'] + + # Test Main + the KeyboardInterrupt exception + with pytest.raises(SystemExit): + main(args) + assert log_has_re('Configuration error: Oh snap!', caplog) + + def test_main_reload_config(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) diff --git a/tests/testdata/testconfigs/test_base_config.json b/tests/testdata/testconfigs/test_base_config.json index d15c5890b..40a114150 100644 --- a/tests/testdata/testconfigs/test_base_config.json +++ b/tests/testdata/testconfigs/test_base_config.json @@ -1,6 +1,6 @@ { "stake_currency": "", - "dry_run": true, + "dry_run": false, "exchange": { "name": "", "key": "",