diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 748efc4d924..00000000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM freqtradeorg/freqtrade:develop_freqairl - -USER root -# Install dependencies -COPY requirements-dev.txt /freqtrade/ - -RUN apt-get update \ - && apt-get -y install --no-install-recommends apt-utils dialog \ - && apt-get -y install --no-install-recommends git sudo vim build-essential \ - && apt-get clean \ - && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ - && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ - && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ - && chown ftuser:ftuser -R /home/ftuser/.local/ \ - && chown ftuser: -R /home/ftuser/ - -USER ftuser - -RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir - -# Empty the ENTRYPOINT to allow all commands -ENTRYPOINT [] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 08b8240b996..b3079198c9d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,42 +1,37 @@ { "name": "freqtrade Develop", - "build": { - "dockerfile": "Dockerfile", - "context": ".." - }, + "image": "ghcr.io/freqtrade/freqtrade-devcontainer:latest", // Use 'forwardPorts' to make a list of ports inside the container available locally. "forwardPorts": [ 8080 ], - "mounts": [ - "source=freqtrade-bashhistory,target=/home/ftuser/commandhistory,type=volume" - ], "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/freqtrade,type=bind,consistency=cached", // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "ftuser", - "onCreateCommand": "pip install --user -e .", "postCreateCommand": "freqtrade create-userdir --userdir user_data/", - "workspaceFolder": "/workspaces/freqtrade", "customizations": { - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "editor.insertSpaces": true, - "files.trimTrailingWhitespace": true, - "[markdown]": { - "files.trimTrailingWhitespace": false, + "vscode": { + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "editor.insertSpaces": true, + "files.trimTrailingWhitespace": true, + "[markdown]": { + "files.trimTrailingWhitespace": false, + }, + "python.pythonPath": "/usr/local/bin/python", }, - "python.pythonPath": "/usr/local/bin/python", - }, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "davidanson.vscode-markdownlint", - "ms-azuretools.vscode-docker", - "vscode-icons-team.vscode-icons", - ], + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-python.isort", + "davidanson.vscode-markdownlint", + "ms-azuretools.vscode-docker", + "vscode-icons-team.vscode-icons", + "github.vscode-github-actions", + ], + } } } diff --git a/.github/.devcontainer/Dockerfile b/.github/.devcontainer/Dockerfile new file mode 100644 index 00000000000..1deab0f54d2 --- /dev/null +++ b/.github/.devcontainer/Dockerfile @@ -0,0 +1,21 @@ +FROM freqtradeorg/freqtrade:develop_freqairl + +USER root +# Install dependencies +COPY requirements-dev.txt /freqtrade/ + +ARG USERNAME=ftuser + +RUN apt-get update \ + && apt-get -y install --no-install-recommends apt-utils dialog git ssh vim build-essential zsh \ + && apt-get clean \ + && mkdir -p /home/${USERNAME}/.vscode-server /home/${USERNAME}/.vscode-server-insiders /home/${USERNAME}/commandhistory \ + && chown ${USERNAME}:${USERNAME} -R /home/${USERNAME}/.local/ \ + && chown ${USERNAME}: -R /home/${USERNAME}/ + +USER ftuser + +RUN pip install --user autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir + +# Empty the ENTRYPOINT to allow all commands +ENTRYPOINT [] diff --git a/.github/.devcontainer/devcontainer.json b/.github/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..d87ea5fdae1 --- /dev/null +++ b/.github/.devcontainer/devcontainer.json @@ -0,0 +1,12 @@ +{ + "name": "freqtrade Dev container image builder", + "build": { + "dockerfile": "Dockerfile", + "context": "../../" + }, + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + }, + "ghcr.io/stuartleeks/dev-container-features/shell-history:0.0.3": {} + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8c9a3f93679..ddea4268405 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,6 +21,9 @@ updates: pytest: patterns: - "pytest*" + mkdocs: + patterns: + - "mkdocs*" - package-ecosystem: "github-actions" directory: "/" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9c03e61c6e..1a87b86cff7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,7 +129,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ "macos-latest", "macos-13", "macos-14" ] + os: [ "macos-12", "macos-13", "macos-14" ] python-version: ["3.9", "3.10", "3.11", "3.12"] exclude: - os: "macos-14" @@ -414,7 +414,7 @@ jobs: pytest --random-order --longrun --durations 20 -n auto - # Notify only once - when CI completes (and after deploy) in case it's successfull + # Notify only once - when CI completes (and after deploy) in case it's successful notify-complete: needs: [ build-linux, diff --git a/.github/workflows/devcontainer-build.yml b/.github/workflows/devcontainer-build.yml new file mode 100644 index 00000000000..6d0a2a482a2 --- /dev/null +++ b/.github/workflows/devcontainer-build.yml @@ -0,0 +1,43 @@ +name: Devcontainer Pre-Build + +on: + workflow_dispatch: + # push: + # branches: + # - "master" + # tags: + # - "v*.*.*" + # pull_requests: + # branches: + # - "master" + +concurrency: + group: "${{ github.workflow }}" + cancel-in-progress: true + +permissions: + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - + name: Checkout + id: checkout + uses: actions/checkout@v4 + - + name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Pre-build dev container image + uses: devcontainers/ci@v0.3 + with: + subFolder: .github + imageName: ghcr.io/${{ github.repository }}-devcontainer + cacheFrom: ghcr.io/${{ github.repository }}-devcontainer + push: always diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ace86868d8..bb7abe31d6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,14 +9,14 @@ repos: # stages: [push] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.9.0" + rev: "v1.10.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.20240311 + - types-requests==2.31.0.20240406 - types-tabulate==0.9.0.20240106 - types-python-dateutil==2.9.0.20240316 - SQLAlchemy==2.0.29 @@ -31,12 +31,12 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.3.4' + rev: 'v0.4.2' hooks: - id: ruff - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: end-of-file-fixer exclude: | @@ -54,3 +54,10 @@ repos: (?x)^( .*\.md )$ + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: + - tomli diff --git a/Dockerfile b/Dockerfile index 6e8992b8bb3..9491f2e0589 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.2-slim-bookworm as base +FROM python:3.12.3-slim-bookworm as base # Setup env ENV LANG C.UTF-8 diff --git a/build_helpers/pre_commit_update.py b/build_helpers/pre_commit_update.py index e6b47d10021..91327ec9ad9 100644 --- a/build_helpers/pre_commit_update.py +++ b/build_helpers/pre_commit_update.py @@ -1,4 +1,4 @@ -# File used in CI to ensure pre-commit dependencies are kept uptodate. +# File used in CI to ensure pre-commit dependencies are kept up-to-date. import sys from pathlib import Path @@ -21,7 +21,7 @@ 'types-') or r.startswith('SQLAlchemy')] with pre_commit_file.open('r') as file: - f = yaml.load(file, Loader=yaml.FullLoader) + f = yaml.load(file, Loader=yaml.SafeLoader) mypy_repo = [repo for repo in f['repos'] if repo['repo'] diff --git a/build_helpers/pyarrow-15.0.2-cp311-cp311-linux_armv7l.whl b/build_helpers/pyarrow-16.0.0-cp311-cp311-linux_armv7l.whl similarity index 65% rename from build_helpers/pyarrow-15.0.2-cp311-cp311-linux_armv7l.whl rename to build_helpers/pyarrow-16.0.0-cp311-cp311-linux_armv7l.whl index 77545d082ad..f8022ffbf01 100644 Binary files a/build_helpers/pyarrow-15.0.2-cp311-cp311-linux_armv7l.whl and b/build_helpers/pyarrow-16.0.0-cp311-cp311-linux_armv7l.whl differ diff --git a/build_helpers/pyarrow-15.0.2-cp39-cp39-linux_armv7l.whl b/build_helpers/pyarrow-16.0.0-cp39-cp39-linux_armv7l.whl similarity index 65% rename from build_helpers/pyarrow-15.0.2-cp39-cp39-linux_armv7l.whl rename to build_helpers/pyarrow-16.0.0-cp39-cp39-linux_armv7l.whl index 638750fc258..c1cbf19de29 100644 Binary files a/build_helpers/pyarrow-15.0.2-cp39-cp39-linux_armv7l.whl and b/build_helpers/pyarrow-16.0.0-cp39-cp39-linux_armv7l.whl differ diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index e91842d6438..563e5df08b8 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -36,7 +36,7 @@ freqtrade backtesting-analysis -c --analysis-groups 0 1 2 3 4 5 ``` This command will read from the last backtesting results. The `--analysis-groups` option is -used to specify the various tabular outputs showing the profit fo each group or trade, +used to specify the various tabular outputs showing the profit of each group or trade, ranging from the simplest (0) to the most detailed per pair, per buy and per sell tag (4): * 0: overall winrate and profit summary by enter_tag diff --git a/docs/backtesting.md b/docs/backtesting.md index 11510dc2464..5fdfd65565e 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -522,8 +522,8 @@ To save time, by default backtest will reuse a cached result from within the las ### Further backtest-result analysis -To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file). -You can then load the trades to perform further analysis as shown in the [data analysis](data-analysis.md#backtesting) backtesting section. +To further analyze your backtest results, freqtrade will export the trades to file by default. +You can then load the trades to perform further analysis as shown in the [data analysis](strategy_analysis_example.md#load-backtest-results-to-pandas-dataframe) backtesting section. ## Assumptions made by backtesting @@ -531,12 +531,13 @@ Since backtesting lacks some detailed information about what happens within a ca - Exchange [trading limits](#trading-limits-in-backtesting) are respected - Entries happen at open-price -- All orders are filled at the requested price (no slippage, no unfilled orders) +- All orders are filled at the requested price (no slippage) as long as the price is within the candle's high/low range - Exit-signal exits happen at open-price of the consecutive candle +- Exits don't free their trade slot for a new trade until the next candle - Exit-signal is favored over Stoploss, because exit-signals are assumed to trigger on candle's open - ROI - - exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%) - - exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit + - Exits are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the exit will be at 2%) + - Exits are never "below the candle", so a ROI of 2% may result in a exit at 2.4% if low was at 2.4% profit - ROI entries which came into effect on the triggering candle (e.g. `120: 0.02` for 1h candles, from `60: 0.05`) will use the candle's open as exit rate - Force-exits caused by `=-1` ROI entries use low as exit value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Stoploss exits happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price @@ -587,7 +588,7 @@ These precision values are based on current exchange limits (as described in the ## Improved backtest accuracy -One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or viceversa?). +One big limitation of backtesting is it's inability to know how prices moved intra-candle (was high before close, or vice-versa?). So assuming you run backtesting with a 1h timeframe, there will be 4 prices for that candle (Open, High, Low, Close). While backtesting does take some assumptions (read above) about this - this can never be perfect, and will always be biased in one way or the other. diff --git a/docs/configuration.md b/docs/configuration.md index 684c6743c36..6e89348e4af 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -197,7 +197,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `max_entry_position_adjustment` | Maximum additional order(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional orders. [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `-1`.*
**Datatype:** Positive Integer or -1 | | **Exchange** -| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
**Datatype:** String +| `exchange.name` | **Required.** Name of the exchange class to use.
**Datatype:** String | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String @@ -252,7 +252,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `False`*.
**Datatype:** Boolean | `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second.
*Defaults to `5` seconds.*
**Datatype:** Positive Integer | `internals.heartbeat_interval` | Print heartbeat message every N seconds. Set to 0 to disable heartbeat messages.
*Defaults to `60` seconds.*
**Datatype:** Positive Integer or 0 -| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
**Datatype:** Boolean +| `internals.sd_notify` | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](advanced-setup.md#configure-the-bot-running-as-a-systemd-service) for more details.
**Datatype:** Boolean | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`.
**Datatype:** ClassName | `strategy_path` | Adds an additional strategy lookup path (must be a directory).
**Datatype:** String | `recursive_strategy_search` | Set to `true` to recursively search sub-directories inside `user_data/strategies` for a strategy.
**Datatype:** Boolean @@ -370,7 +370,7 @@ This setting works in combination with `max_open_trades`. The maximum capital en For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`. !!! Note - This setting respects the [available balance configuration](#available-balance). + This setting respects the [available balance configuration](#tradable-balance). #### Dynamic stake amount @@ -547,7 +547,7 @@ is automatically cancelled by the exchange. **PO (Post only):** Post only order. The order is either placed as a maker order, or it is canceled. -This means the order must be placed on orderbook for at at least time in an unfilled state. +This means the order must be placed on orderbook for at least time in an unfilled state. #### time_in_force config diff --git a/docs/developer.md b/docs/developer.md index 9e23db656ed..705e8d11600 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -83,7 +83,7 @@ Details will obviously vary between setups - but this should work to get you sta ``` json { "name": "freqtrade trade", - "type": "python", + "type": "debugpy", "request": "launch", "module": "freqtrade", "console": "integratedTerminal", @@ -261,7 +261,7 @@ For that reason, they must implement the following methods: The `until` portion should be calculated using the provided `calculate_lock_end()` method. -All Protections should use `"stop_duration"` / `"stop_duration_candles"` to define how long a a pair (or all pairs) should be locked. +All Protections should use `"stop_duration"` / `"stop_duration_candles"` to define how long a pair (or all pairs) should be locked. The content of this is made available as `self._stop_duration` to the each Protection. If your protection requires a look-back period, please use `"lookback_period"` / `"lockback_period_candles"` to keep all protections aligned. @@ -305,7 +305,7 @@ The `IProtection` parent class provides a helper method for this in `calculate_l Most exchanges supported by CCXT should work out of the box. -To quickly test the public endpoints of an exchange, add a configuration for your exchange to `test_ccxt_compat.py` and run these tests with `pytest --longrun tests/exchange/test_ccxt_compat.py`. +To quickly test the public endpoints of an exchange, add a configuration for your exchange to `tests/exchange_online/conftest.py` and run these tests with `pytest --longrun tests/exchange_online/test_ccxt_compat.py`. Completing these tests successfully a good basis point (it's a requirement, actually), however these won't guarantee correct exchange functioning, as this only tests public endpoints, but no private endpoint (like generate order or similar). Also try to use `freqtrade download-data` for an extended timerange (multiple months) and verify that the data downloaded correctly (no holes, the specified timerange was actually downloaded). diff --git a/docs/edge.md b/docs/edge.md index bb702f202e4..fe33f141b1c 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -137,7 +137,7 @@ $$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_ ### Expectancy -By combining the Win Rate $W$ and and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows: +By combining the Win Rate $W$ and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows: $$E = R * W - L$$ diff --git a/docs/exchanges.md b/docs/exchanges.md index a8c4a8b4fb4..d4437cfff20 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -299,7 +299,7 @@ $ pip3 install web3 Most exchanges return current incomplete candle via their OHLCV/klines API interface. By default, Freqtrade assumes that incomplete candle is fetched from the exchange and removes the last candle assuming it's the incomplete candle. -Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. +Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#incomplete-candles) from the Contributor documentation. Due to the danger of repainting, Freqtrade does not allow you to use this incomplete candle. diff --git a/docs/faq.md b/docs/faq.md index 95a9924f983..13476557949 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,7 +2,7 @@ ## Supported Markets -Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges. +Freqtrade supports spot trading, as well as (isolated) futures trading for some selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges. ### Can my bot open short positions? @@ -14,7 +14,7 @@ In spot markets, you can in some cases use leveraged spot tokens, which reflect ### Can my bot trade options or futures? -Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an uptodate list of supported exchanges. +Futures trading is supported for selected exchanges. Please refer to the [documentation start page](index.md#supported-futures-exchanges-experimental) for an up-to-date list of supported exchanges. ## Beginner Tips & Tricks diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 6a1537d911d..311127dd7dc 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -235,7 +235,7 @@ By default, FreqAI builds a dynamic pipeline based on user congfiguration settin Users are encouraged to customize the data pipeline to their needs by building their own data pipeline. This can be done by simply setting `dk.feature_pipeline` to their desired `Pipeline` object inside their `IFreqaiModel` `train()` function, or if they prefer not to touch the `train()` function, they can override `define_data_pipeline`/`define_label_pipeline` functions in their `IFreqaiModel`: !!! note "More information available" - FreqAI uses the the [`DataSieve`](https://github.com/emergentmethods/datasieve) pipeline, which follows the SKlearn pipeline API, but adds, among other features, coherence between the X, y, and sample_weight vector point removals, feature removal, feature name following. + FreqAI uses the [`DataSieve`](https://github.com/emergentmethods/datasieve) pipeline, which follows the SKlearn pipeline API, but adds, among other features, coherence between the X, y, and sample_weight vector point removals, feature removal, feature name following. ```python from datasieve.transforms import SKLearnWrapper, DissimilarityIndex diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 055b7b45df3..56043bb0f38 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -31,7 +31,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `feature_parameters` | A dictionary containing the parameters used to engineer the feature set. Details and examples are shown [here](freqai-feature-engineering.md).
**Datatype:** Dictionary. | `include_timeframes` | A list of timeframes that all indicators in `feature_engineering_expand_*()` will be created for. The list is added as features to the base indicators dataset.
**Datatype:** List of timeframes (strings). | `include_corr_pairlist` | A list of correlated coins that FreqAI will add as additional features to all `pair_whitelist` coins. All indicators set in `feature_engineering_expand_*()` during feature engineering (see details [here](freqai-feature-engineering.md)) will be created for each correlated coin. The correlated coins features are added to the base indicators dataset.
**Datatype:** List of assets (strings). -| `label_period_candles` | Number of candles into the future that the labels are created for. This is used in `feature_engineering_expand_all()` (see `templates/FreqaiExampleStrategy.py` for detailed usage). You can create custom labels and choose whether to make use of this parameter or not.
**Datatype:** Positive integer. +| `label_period_candles` | Number of candles into the future that the labels are created for. This can be used in `set_freqai_targets()` (see `templates/FreqaiExampleStrategy.py` for detailed usage). This parameter is not necessarily required, you can create custom labels and choose whether to make use of this parameter or not. Please see `templates/FreqaiExampleStrategy.py` to see the example usage.
**Datatype:** Positive integer. | `include_shifted_candles` | Add features from previous candles to subsequent candles with the intent of adding historical information. If used, FreqAI will duplicate and shift all features from the `include_shifted_candles` previous candles so that the information is available for the subsequent candle.
**Datatype:** Positive integer. | `weight_factor` | Weight training data points according to their recency (see details [here](freqai-feature-engineering.md#weighting-features-for-temporal-importance)).
**Datatype:** Positive float (typically < 1). | `indicator_max_period_candles` | **No longer used (#7325)**. Replaced by `startup_candle_count` which is set in the [strategy](freqai-configuration.md#building-a-freqai-strategy). `startup_candle_count` is timeframe independent and defines the maximum *period* used in `feature_engineering_*()` for indicator creation. FreqAI uses this parameter together with the maximum timeframe in `include_time_frames` to calculate how many data points to download such that the first data point does not include a NaN.
**Datatype:** Positive integer. @@ -55,7 +55,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | | **Data split parameters within the `freqai.data_split_parameters` sub dictionary** | `data_split_parameters` | Include any additional parameters available from scikit-learn `test_train_split()`, which are shown [here](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html) (external website).
**Datatype:** Dictionary. | `test_size` | The fraction of data that should be used for testing instead of training.
**Datatype:** Positive float < 1. -| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`.
**Datatype:** Boolean.
Defaut: `False`. +| `shuffle` | Shuffle the training data points during training. Typically, to not remove the chronological order of data in time-series forecasting, this is set to `False`.
**Datatype:** Boolean.
Default: `False`. ### Model training parameters diff --git a/docs/hyperopt.md b/docs/hyperopt.md index d3371d77109..f88928344cc 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -14,8 +14,7 @@ To learn how to get data for the pairs and exchange you're interested in, head o !!! Note Since 2021.4 release you no longer have to write a separate hyperopt class, but can configure the parameters directly in the strategy. - The legacy method is still supported, but it is no longer the recommended way of setting up hyperopt. - The legacy documentation is available at [Legacy Hyperopt](advanced-hyperopt.md#legacy-hyperopt). + The legacy method was supported up to 2021.8 and has been removed in 2021.9. ## Install hyperopt dependencies @@ -765,7 +764,7 @@ Override the `roi_space()` method if you need components of the ROI tables to va A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Stoploss results @@ -807,7 +806,7 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Trailing Stop results diff --git a/docs/installation.md b/docs/installation.md index a87a3ff4e9e..189f45cfae8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -51,7 +51,7 @@ These requirements apply to both [Script Installation](#script-installation) and ### Install code We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. -OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. +OS Specific steps are listed first, the common section below is necessary for all systems. !!! Note Python3.9 or higher and the corresponding pip are assumed to be available. diff --git a/docs/leverage.md b/docs/leverage.md index 09ebf1075cb..2fbd13145d7 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -17,7 +17,7 @@ If you already have an existing strategy, please read the [strategy migration gu ## Shorting -Shorting is not possible when trading with [`trading_mode`](#understand-tradingmode) set to `spot`. To short trade, `trading_mode` must be set to `margin`(currently unavailable) or [`futures`](#futures), with [`margin_mode`](#margin-mode) set to `cross`(currently unavailable) or [`isolated`](#isolated-margin-mode) +Shorting is not possible when trading with [`trading_mode`](#leverage-trading-modes) set to `spot`. To short trade, `trading_mode` must be set to `margin`(currently unavailable) or [`futures`](#futures), with [`margin_mode`](#margin-mode) set to `cross`(currently unavailable) or [`isolated`](#isolated-margin-mode) For a strategy to short, the strategy class must set the class variable `can_short = True` diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 7fafe159840..09a2dc10470 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.6 -mkdocs==1.5.3 -mkdocs-material==9.5.15 +mkdocs==1.6.0 +mkdocs-material==9.5.19 mdx_truly_sane_lists==1.3 -pymdown-extensions==10.7.1 +pymdown-extensions==10.8.1 jinja2==3.1.3 diff --git a/docs/rest-api.md b/docs/rest-api.md index 69a0f31f559..ab5e9db9f74 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -89,7 +89,8 @@ Make sure that the following 2 lines are available in your docker-compose file: ``` !!! Danger "Security warning" - By using `8080:8080` in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot. + By using `"8080:8080"` (or `"0.0.0.0:8080:8080"`) in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot. + This **may** be safe if you're running the bot in a secure environment (like your home network), but it's not recommended to expose the API to the internet. ## Rest API @@ -166,6 +167,7 @@ freqtrade-client --config rest_config.json [optional parameters] | `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional. | `locks` | Displays currently locked pairs. | `delete_lock ` | Deletes (disables) the lock by id. +| `locks add , , [side], [reason]` | Locks a pair until "until". (Until will be rounded up to the nearest timeframe). | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `forceexit ` | Instantly exits the given trade (Ignoring `minimum_roi`). | `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`). @@ -453,7 +455,7 @@ To properly configure your reverse proxy (securely), please consult it's documen - **Caddy**: Caddy v2 supports websockets out of the box, see the [documentation](https://caddyserver.com/docs/v2-upgrade#proxy) !!! Tip "SSL certificates" - You can use tools like certbot to setup ssl certificates to access your bot's UI through encrypted connection by using any fo the above reverse proxies. + You can use tools like certbot to setup ssl certificates to access your bot's UI through encrypted connection by using any of the above reverse proxies. While this will protect your data in transit, we do not recommend to run the freqtrade API outside of your private network (VPN, SSH tunnel). ### OpenAPI interface diff --git a/docs/stoploss.md b/docs/stoploss.md index 935950d06ab..a1095b46551 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -158,7 +158,7 @@ You could also have a default stop loss when you are in the red with your buy (b For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. !!! Note - If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). + If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value. @@ -240,7 +240,7 @@ When using leverage, the same principle is applied - with stoploss defining the Therefore, a stoploss of 10% on a 10x trade would trigger on a 1% price move. If your stake amount (own capital) was 100$ - this trade would be 1000$ at 10x (after leverage). -If price moves 1% - you've lost 10$ of your own capital - therfore stoploss will trigger in this case. +If price moves 1% - you've lost 10$ of your own capital - therefore stoploss will trigger in this case. Make sure to be aware of this, and avoid using too tight stoploss (at 10x leverage, 10% risk may be too little to allow the trade to "breath" a little). diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index debd5bc1bb5..3cd0259f475 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -209,7 +209,7 @@ def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_r ## Exit tag -Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag. +Similar to [Entry Tagging](#enter-tag), you can also specify an exit tag. ``` python def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -326,4 +326,4 @@ for val in self.buy_ema_short.range: dataframe = pd.concat(frames, axis=1) ``` -Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existant. +Freqtrade does however also counter this by running `dataframe.copy()` on the dataframe right after the `populate_indicators()` method - so performance implications of this should be low to non-existent. diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 74dc406516e..b1e46d35603 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -167,7 +167,7 @@ During backtesting, `current_rate` (and `current_profit`) are provided against t The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss. -Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). +Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)). !!! Note "Use of dates" All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. @@ -332,7 +332,7 @@ class AwesomeStrategy(IStrategy): **kwargs) -> Optional[float]: if current_profit < 0.04: - return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss + return None # return None to keep using the initial stoploss # After reaching the desired offset, allow the stoploss to trail by half the profit desired_stoploss = current_profit / 2 @@ -450,7 +450,7 @@ Stoploss values returned from `custom_stoploss()` must specify a percentage rela ``` - Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. + Full examples can be found in the [Custom stoploss](strategy-callbacks.md#custom-stoploss) section of the Documentation. !!! Note Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings. @@ -809,6 +809,7 @@ Returning a value more than the above (so remaining stake_amount would become ne ``` python from freqtrade.persistence import Trade +from typing import Optional, Tuple, Union class DigDeeperStrategy(IStrategy): @@ -948,7 +949,7 @@ If the cancellation of the original order fails, then the order will not be repl ```python from freqtrade.persistence import Trade -from datetime import timedelta +from datetime import timedelta, datetime class AwesomeStrategy(IStrategy): diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 900dafb33c7..48f629df5c6 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -405,7 +405,7 @@ The metadata-dict (available for `populate_entry_trend`, `populate_exit_trend`, Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. The Metadata-dict should not be modified and does not persist information across multiple calls. -Instead, have a look at the [Storing information](strategy-advanced.md#Storing-information) section. +Instead, have a look at the [Storing information](strategy-advanced.md#storing-information-persistent) section. ## Strategy file loading @@ -551,8 +551,8 @@ for more information. # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom - # formatting. Available in populate_indicators and other methods as 'rsi_upper'. - @informative('1h', 'BTC/{stake}', '{column}') + # formatting. Available in populate_indicators and other methods as 'rsi_upper_1h'. + @informative('1h', 'BTC/{stake}', '{column}_{timeframe}') def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) return dataframe @@ -776,7 +776,7 @@ The orderbook structure is aligned with the order structure from [ccxt](https:// Therefore, using `ob['bids'][0][0]` as demonstrated above will result in using the best bid price. `ob['bids'][0][1]` would look at the amount at this orderbook position. !!! Warning "Warning about backtesting" - The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values. + The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return up-to-date values. ### *ticker(pair)* diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f878fc2b395..377479a90bb 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -53,7 +53,7 @@ You can use bots in telegram groups by just adding them to the group. You can fi } ``` -For the Freqtrade configuration, you can then use the the full value (including `-` if it's there) as string: +For the Freqtrade configuration, you can then use the full value (including `-` if it's there) as string: ```json "chat_id": "-1001332619709" diff --git a/docs/trade-object.md b/docs/trade-object.md index 15a8b1938f7..fa8b2dbb1c9 100644 --- a/docs/trade-object.md +++ b/docs/trade-object.md @@ -126,7 +126,7 @@ An `Order` object will always be tied to it's corresponding [`Trade`](#trade-obj ### Order - Available attributes an Order object is typically attached to a trade. -Most properties here can be None as they are dependant on the exchange response. +Most properties here can be None as they are dependent on the exchange response. | Attribute | DataType | Description | |------------|-------------|-------------| @@ -141,7 +141,7 @@ Most properties here can be None as they are dependant on the exchange response. `amount` | float | Amount in base currency `filled` | float | Filled amount (in base currency) `remaining` | float | Remaining amount -`cost` | float | Cost of the order - usually average * filled (*Exchange dependant on futures, may contain the cost with or without leverage and may be in contracts.*) +`cost` | float | Cost of the order - usually average * filled (*Exchange dependent on futures, may contain the cost with or without leverage and may be in contracts.*) `stake_amount` | float | Stake amount used for this order. *Added in 2023.7.* `order_date` | datetime | Order creation date **use `order_date_utc` instead** `order_date_utc` | datetime | Order creation date (in UTC) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 347f14a07f9..aa3f9245565 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2024.3' +__version__ = '2024.4' if 'dev' in __version__: from pathlib import Path diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 55c3aa58628..d994c09c059 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -197,7 +197,10 @@ def _build_subcommands(self) -> None: self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group) # Build main command - self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') + self.parser = argparse.ArgumentParser( + prog="freqtrade", + description='Free, open source crypto trading bot' + ) self._build_args(optionlist=['version'], parser=self.parser) from freqtrade.commands import (start_analysis_entries_exits, start_backtesting, diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index c87f55e4359..8de600c9e2a 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -16,6 +16,10 @@ logger = logging.getLogger(__name__) +# Timeout for requests +req_timeout = 30 + + def start_create_userdir(args: Dict[str, Any]) -> None: """ Create "user_data" directory to contain user data strategies, hyperopt, ...) @@ -119,7 +123,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): from zipfile import ZipFile logger.info(f"Downloading {dl_url}") - resp = requests.get(dl_url).content + resp = requests.get(dl_url, timeout=req_timeout).content dest_folder.mkdir(parents=True, exist_ok=True) with ZipFile(BytesIO(resp)) as zf: for fn in zf.filelist: @@ -137,7 +141,7 @@ def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]: base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path - resp = requests.get(f"{base_url}releases") + resp = requests.get(f"{base_url}releases", timeout=req_timeout) resp.raise_for_status() r = resp.json() @@ -158,7 +162,7 @@ def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]: # URL not found - try assets url if not dl_url: assets = r[0]['assets_url'] - resp = requests.get(assets) + resp = requests.get(assets, timeout=req_timeout) r = resp.json() dl_url = r[0]['browser_download_url'] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 0da30717593..906d0a54466 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -13,7 +13,7 @@ from freqtrade.configuration.environment_vars import enironment_vars_to_dict from freqtrade.configuration.load_config import load_file, load_from_files from freqtrade.constants import Config -from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, CandleType, RunMode, TradingMode +from freqtrade.enums import NON_UTIL_MODES, TRADE_MODES, CandleType, RunMode, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging @@ -127,7 +127,7 @@ def _process_logging_options(self, config: Config) -> None: setup_logging(config) def _process_trading_options(self, config: Config) -> None: - if config['runmode'] not in TRADING_MODES: + if config['runmode'] not in TRADE_MODES: return if config.get('dry_run', False): @@ -202,7 +202,7 @@ def _process_datadir_options(self, config: Config) -> None: if self.args.get('show_sensitive'): logger.warning( - "Sensitive information will be shown in the upcomming output. " + "Sensitive information will be shown in the upcoming output. " "Please make sure to never share this output without redacting " "the information yourself.") diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index ef92d4db62e..07417b27f7a 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -238,6 +238,16 @@ def update_backtest_metadata(filename: Path, strategy: str, content: Dict[str, A file_dump_json(get_backtest_metadata_filename(filename), metadata) +def get_backtest_market_change(filename: Path, include_ts: bool = True) -> pd.DataFrame: + """ + Read backtest market change file. + """ + df = pd.read_feather(filename) + if include_ts: + df.loc[:, '__date_ts'] = df.loc[:, 'date'].astype(np.int64) // 1000 // 1000 + return df + + def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]: """ diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index b737007c474..6fa6e473828 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -523,7 +523,7 @@ def send_msg(self, message: str, *, always_send: bool = False) -> None: Send custom RPC Notifications from your bot. Will not send any bot in modes other than Dry-run or Live. :param message: Message to be sent. Must be below 4096. - :param always_send: If False, will send the message only once per candle, and surpress + :param always_send: If False, will send the message only once per candle, and suppress identical messages. Careful as this can end up spaming your chat. Defaults to False diff --git a/freqtrade/data/history/datahandlers/idatahandler.py b/freqtrade/data/history/datahandlers/idatahandler.py index fbaded6402b..cff26760f2b 100644 --- a/freqtrade/data/history/datahandlers/idatahandler.py +++ b/freqtrade/data/history/datahandlers/idatahandler.py @@ -302,8 +302,8 @@ def rebuild_pair_from_filename(pair: str) -> str: Rebuild pair name from filename Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names. """ - res = re.sub(r'^(([A-Za-z\d]{1,10})|^([A-Za-z\-]{1,6}))(_)', r'\g<1>/', pair, 1) - res = re.sub('_', ':', res, 1) + res = re.sub(r'^(([A-Za-z\d]{1,10})|^([A-Za-z\-]{1,6}))(_)', r'\g<1>/', pair, count=1) + res = re.sub('_', ':', res, count=1) return res def ohlcv_load(self, pair, timeframe: str, diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 738129939e9..43a33fa0df0 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -30,18 +30,53 @@ def calculate_market_change(data: Dict[str, pd.DataFrame], column: str = "close" return float(np.mean(tmp_means)) -def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame], - column: str = "close") -> pd.DataFrame: +def combine_dataframes_by_column( + data: Dict[str, pd.DataFrame], column: str = "close") -> pd.DataFrame: """ Combine multiple dataframes "column" :param data: Dict of Dataframes, dict key should be pair. :param column: Column in the original dataframes to use - :return: DataFrame with the column renamed to the dict key, and a column - named mean, containing the mean of all pairs. + :return: DataFrame with the column renamed to the dict key. :raise: ValueError if no data is provided. """ + if not data: + raise ValueError("No data provided.") df_comb = pd.concat([data[pair].set_index('date').rename( {column: pair}, axis=1)[pair] for pair in data], axis=1) + return df_comb + + +def combined_dataframes_with_rel_mean( + data: Dict[str, pd.DataFrame], fromdt: datetime, todt: datetime, + column: str = "close") -> pd.DataFrame: + """ + Combine multiple dataframes "column" + :param data: Dict of Dataframes, dict key should be pair. + :param column: Column in the original dataframes to use + :return: DataFrame with the column renamed to the dict key, and a column + named mean, containing the mean of all pairs. + :raise: ValueError if no data is provided. + """ + df_comb = combine_dataframes_by_column(data, column) + # Trim dataframes to the given timeframe + df_comb = df_comb.iloc[(df_comb.index >= fromdt) & (df_comb.index < todt)] + df_comb['count'] = df_comb.count(axis=1) + df_comb['mean'] = df_comb.mean(axis=1) + df_comb['rel_mean'] = df_comb['mean'].pct_change().fillna(0).cumsum() + return df_comb[['mean', 'rel_mean', 'count']] + + +def combine_dataframes_with_mean( + data: Dict[str, pd.DataFrame], column: str = "close") -> pd.DataFrame: + """ + Combine multiple dataframes "column" + :param data: Dict of Dataframes, dict key should be pair. + :param column: Column in the original dataframes to use + :return: DataFrame with the column renamed to the dict key, and a column + named mean, containing the mean of all pairs. + :raise: ValueError if no data is provided. + """ + df_comb = combine_dataframes_by_column(data, column) df_comb['mean'] = df_comb.mean(axis=1) diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 69ef345e81c..55ab130aeb6 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -9,7 +9,7 @@ from freqtrade.enums.ordertypevalue import OrderTypeValues from freqtrade.enums.pricetype import PriceType from freqtrade.enums.rpcmessagetype import NO_ECHO_MESSAGES, RPCMessageType, RPCRequestType -from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode +from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADE_MODES, RunMode from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode diff --git a/freqtrade/enums/runmode.py b/freqtrade/enums/runmode.py index 6545aaec7d7..17caea46655 100644 --- a/freqtrade/enums/runmode.py +++ b/freqtrade/enums/runmode.py @@ -18,6 +18,6 @@ class RunMode(Enum): OTHER = "other" -TRADING_MODES = [RunMode.LIVE, RunMode.DRY_RUN] +TRADE_MODES = [RunMode.LIVE, RunMode.DRY_RUN] OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT] -NON_UTIL_MODES = TRADING_MODES + OPTIMIZE_MODES +NON_UTIL_MODES = TRADE_MODES + OPTIMIZE_MODES diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 1c6ba9cbd26..109f3c1e8e1 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -4,6 +4,7 @@ from freqtrade.exchange.exchange import Exchange # isort: on from freqtrade.exchange.binance import Binance +from freqtrade.exchange.bingx import Bingx from freqtrade.exchange.bitmart import Bitmart from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bitvavo import Bitvavo @@ -24,6 +25,7 @@ from freqtrade.exchange.gate import Gate from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.htx import Htx +from freqtrade.exchange.idex import Idex from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kucoin import Kucoin from freqtrade.exchange.okx import Okx diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 89b983d919f..8cfe52d5103 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -84,7 +84,7 @@ def additional_exchange_init(self) -> None: raise OperationalException(msg) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' ) from e @@ -210,7 +210,7 @@ def load_leverage_tiers(self) -> Dict[str, List[Dict]]: return self._api.fetch_leverage_tiers() except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not fetch leverage amounts due to' f'{e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 09076c57057..abc13539534 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -21,95 +21,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, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "50000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "150.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "500000", + "notionalFloor": "50000", "maintMarginRatio": "0.05", - "cum": "675.0" + "cum": "1400.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "500000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "26400.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 1000000.0, + "maxNotional": 1250000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "1250000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "51400.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 1250000.0, + "maxNotional": 2500000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "2500000", + "notionalFloor": "1250000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "207650.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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" } } ], @@ -623,95 +639,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, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "50000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "150.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "500000", + "notionalFloor": "50000", "maintMarginRatio": "0.05", - "cum": "675.0" + "cum": "1400.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "500000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "26400.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 1000000.0, + "maxNotional": 1250000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "1250000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "51400.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 1250000.0, + "maxNotional": 2500000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "2500000", + "notionalFloor": "1250000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "207650.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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" } } ], @@ -1874,96 +1906,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", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.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": "275.0" } }, { "tier": 4.0, "currency": "USDT", "minNotional": 100000.0, - "maxNotional": 200000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1000000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2775.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "52775.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 2000000.0, + "maxNotional": 2500000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "2500000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "102775.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2500000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "5000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "415275.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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": "1665275.0" } } ], @@ -3595,6 +3643,136 @@ } } ], + "ARB/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDC", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "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": "770.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": "15770.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": "75770.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "150770.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "775770.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3775770.0" + } + } + ], "ARB/USDT:USDT": [ { "tier": 1.0, @@ -5743,6 +5921,168 @@ } } ], + "BCH/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": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 40.0, + "info": { + "bracket": "3", + "initialLeverage": "40", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.01", + "cum": "42.5" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "minNotional": 50000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "4", + "initialLeverage": "25", + "notionalCap": "500000", + "notionalFloor": "50000", + "maintMarginRatio": "0.02", + "cum": "542.5" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "minNotional": 500000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "2000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.05", + "cum": "15542.5" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.1", + "cum": "115542.5" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "8000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.125", + "cum": "215542.5" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 8000000.0, + "maxNotional": 15000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "8", + "initialLeverage": "3", + "notionalCap": "15000000", + "notionalFloor": "8000000", + "maintMarginRatio": "0.15", + "cum": "415542.5" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 15000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "20000000", + "notionalFloor": "15000000", + "maintMarginRatio": "0.25", + "cum": "1915542.5" + } + }, + { + "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": "6915542.5" + } + } + ], "BCH/USDT:USDT": [ { "tier": 1.0, @@ -7207,6 +7547,136 @@ } } ], + "BOME/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" + } + } + ], "BOME/USDT:USDT": [ { "tier": 1.0, @@ -7228,96 +7698,112 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 150000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { - "bracket": "2", + "bracket": "3", "initialLeverage": "20", - "notionalCap": "25000", - "notionalFloor": "5000", + "notionalCap": "150000", + "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "50.0" + "cum": "275.0" } }, { - "tier": 3.0, + "tier": 4.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, + "minNotional": 150000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { - "bracket": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "1500000", + "notionalFloor": "150000", "maintMarginRatio": "0.05", - "cum": "675.0" + "cum": "4025.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, + "minNotional": 1500000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "3000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "79025.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 3000000.0, + "maxNotional": 3750000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "3750000", + "notionalFloor": "3000000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "154025.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 3750000.0, + "maxNotional": 7500000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "7500000", + "notionalFloor": "3750000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "622775.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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": "2497775.0" } } ], @@ -12059,6 +12545,136 @@ } } ], + "ENA/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": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "100000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "275.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1000000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2775.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.1", + "cum": "52775.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 2500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "2500000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "102775.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "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": "415275.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "10000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.5", + "cum": "1665275.0" + } + } + ], "ENJ/USDT:USDT": [ { "tier": 1.0, @@ -13410,96 +14026,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", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.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": "275.0" } }, { "tier": 4.0, "currency": "USDT", "minNotional": 100000.0, - "maxNotional": 200000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1000000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2775.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "52775.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 2000000.0, + "maxNotional": 2500000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "2500000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "102775.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2500000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "5000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "415275.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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": "1665275.0" } } ], @@ -13731,6 +14363,152 @@ } } ], + "FIL/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "minNotional": 0.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "50000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDC", + "minNotional": 50000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 40.0, + "info": { + "bracket": "2", + "initialLeverage": "40", + "notionalCap": "250000", + "notionalFloor": "50000", + "maintMarginRatio": "0.01", + "cum": "200.0" + } + }, + { + "tier": 3.0, + "currency": "USDC", + "minNotional": 250000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "600000", + "notionalFloor": "250000", + "maintMarginRatio": "0.02", + "cum": "2700.0" + } + }, + { + "tier": 4.0, + "currency": "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": "20700.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": "80700.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.125", + "cum": "155700.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.165, + "maxLeverage": 3.0, + "info": { + "bracket": "7", + "initialLeverage": "3", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.165", + "cum": "395700.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.25", + "cum": "1245700.0" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.5", + "cum": "6245700.0" + } + } + ], "FIL/USDT:USDT": [ { "tier": 1.0, @@ -17712,96 +18490,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", - "cum": "50.0" + "maintMarginRatio": "0.02", + "cum": "25.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": "275.0" } }, { "tier": 4.0, "currency": "USDT", "minNotional": 100000.0, - "maxNotional": 200000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1000000", + "notionalFloor": "100000", + "maintMarginRatio": "0.05", + "cum": "2775.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "52775.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 2000000.0, + "maxNotional": 2500000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "2500000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "102775.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2500000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "5000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "415275.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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": "1665275.0" } } ], @@ -19873,6 +20667,168 @@ } } ], + "LTC/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": 750000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "4", + "initialLeverage": "25", + "notionalCap": "750000", + "notionalFloor": "50000", + "maintMarginRatio": "0.02", + "cum": "545.0" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "minNotional": 750000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "3000000", + "notionalFloor": "750000", + "maintMarginRatio": "0.05", + "cum": "23045.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "10000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.1", + "cum": "173045.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 10000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "12000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.125", + "cum": "423045.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "8", + "initialLeverage": "3", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.15", + "cum": "723045.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": "2723045.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": "10223045.0" + } + } + ], "LTC/USDT:USDT": [ { "tier": 1.0, @@ -20637,6 +21593,152 @@ } } ], + "MATIC/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "minNotional": 0.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "10000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDC", + "minNotional": 10000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.007, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "25000", + "notionalFloor": "10000", + "maintMarginRatio": "0.007", + "cum": "10.0" + } + }, + { + "tier": 3.0, + "currency": "USDC", + "minNotional": 25000.0, + "maxNotional": 600000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "3", + "initialLeverage": "25", + "notionalCap": "600000", + "notionalFloor": "25000", + "maintMarginRatio": "0.01", + "cum": "85.0" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "minNotional": 600000.0, + "maxNotional": 900000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "4", + "initialLeverage": "20", + "notionalCap": "900000", + "notionalFloor": "600000", + "maintMarginRatio": "0.025", + "cum": "9085.0" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "minNotional": 900000.0, + "maxNotional": 1800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "1800000", + "notionalFloor": "900000", + "maintMarginRatio": "0.05", + "cum": "31585.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 1800000.0, + "maxNotional": 4800000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "4800000", + "notionalFloor": "1800000", + "maintMarginRatio": "0.1", + "cum": "121585.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 4800000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "6000000", + "notionalFloor": "4800000", + "maintMarginRatio": "0.125", + "cum": "241585.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 6000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "8", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.25", + "cum": "991585.0" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "9", + "initialLeverage": "1", + "notionalCap": "30000000", + "notionalFloor": "18000000", + "maintMarginRatio": "0.5", + "cum": "5491585.0" + } + } + ], "MATIC/USDT:USDT": [ { "tier": 1.0, @@ -22069,6 +23171,136 @@ } } ], + "NEAR/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "minNotional": 0.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDC", + "minNotional": 10000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "250000", + "notionalFloor": "10000", + "maintMarginRatio": "0.025", + "cum": "100.0" + } + }, + { + "tier": 3.0, + "currency": "USDC", + "minNotional": 250000.0, + "maxNotional": 750000.0, + "maintenanceMarginRate": 0.03, + "maxLeverage": 15.0, + "info": { + "bracket": "3", + "initialLeverage": "15", + "notionalCap": "750000", + "notionalFloor": "250000", + "maintMarginRatio": "0.03", + "cum": "1350.0" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "minNotional": 750000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "1500000", + "notionalFloor": "750000", + "maintMarginRatio": "0.05", + "cum": "16350.0" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "minNotional": 1500000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "4000000", + "notionalFloor": "1500000", + "maintMarginRatio": "0.1", + "cum": "91350.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 4000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.125", + "cum": "191350.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "816350.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3816350.0" + } + } + ], "NEAR/USDT:USDT": [ { "tier": 1.0, @@ -22199,6 +23431,136 @@ } } ], + "NEO/USDC:USDC": [ + { + "tier": 1.0, + "currency": "USDC", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.006", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDC", + "minNotional": 5000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.01", + "cum": "20.0" + } + }, + { + "tier": 3.0, + "currency": "USDC", + "minNotional": 50000.0, + "maxNotional": 400000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "400000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "770.0" + } + }, + { + "tier": 4.0, + "currency": "USDC", + "minNotional": 400000.0, + "maxNotional": 800000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "800000", + "notionalFloor": "400000", + "maintMarginRatio": "0.05", + "cum": "10770.0" + } + }, + { + "tier": 5.0, + "currency": "USDC", + "minNotional": 800000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "2000000", + "notionalFloor": "800000", + "maintMarginRatio": "0.1", + "cum": "50770.0" + } + }, + { + "tier": 6.0, + "currency": "USDC", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.125", + "cum": "100770.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 5000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "12000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "725770.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.5", + "cum": "3725770.0" + } + } + ], "NEO/USDT:USDT": [ { "tier": 1.0, @@ -23209,7 +24571,7 @@ } } ], - "ONDO/USDT:USDT": [ + "OMNI/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -23231,12 +24593,126 @@ "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" + } + } + ], + "ONDO/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": 50000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "2", "initialLeverage": "25", - "notionalCap": "25000", + "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.02", "cum": "25.0" @@ -23245,97 +24721,97 @@ { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 80000.0, + "minNotional": 50000.0, + "maxNotional": 150000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "80000", - "notionalFloor": "25000", + "notionalCap": "150000", + "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "150.0" + "cum": "275.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 80000.0, - "maxNotional": 800000.0, + "minNotional": 150000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "800000", - "notionalFloor": "80000", + "notionalCap": "1500000", + "notionalFloor": "150000", "maintMarginRatio": "0.05", - "cum": "2150.0" + "cum": "4025.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 800000.0, - "maxNotional": 1600000.0, + "minNotional": 1500000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "1600000", - "notionalFloor": "800000", + "notionalCap": "3000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.1", - "cum": "42150.0" + "cum": "79025.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1600000.0, - "maxNotional": 2000000.0, + "minNotional": 3000000.0, + "maxNotional": 3750000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "2000000", - "notionalFloor": "1600000", + "notionalCap": "3750000", + "notionalFloor": "3000000", "maintMarginRatio": "0.125", - "cum": "82150.0" + "cum": "154025.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 4000000.0, + "minNotional": 3750000.0, + "maxNotional": 7500000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "4000000", - "notionalFloor": "2000000", + "notionalCap": "7500000", + "notionalFloor": "3750000", "maintMarginRatio": "0.25", - "cum": "332150.0" + "cum": "622775.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 4000000.0, - "maxNotional": 8000000.0, + "minNotional": 7500000.0, + "maxNotional": 15000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "8000000", - "notionalFloor": "4000000", + "notionalCap": "15000000", + "notionalFloor": "7500000", "maintMarginRatio": "0.5", - "cum": "1332150.0" + "cum": "2497775.0" } } ], @@ -24353,14 +25829,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" } }, @@ -24369,14 +25845,14 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "25", "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "25.0" } }, @@ -24384,80 +25860,96 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 200000.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": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "200000", - "notionalFloor": "25000", + "notionalCap": "800000", + "notionalFloor": "80000", "maintMarginRatio": "0.05", - "cum": "650.0" + "cum": "2150.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "1600000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "10650.0" + "cum": "42150.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "23150.0" + "cum": "82150.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "4000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "148150.0" + "cum": "332150.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 3000000.0, - "maxNotional": 5000000.0, + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "3000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "898150.0" + "cum": "1332150.0" } } ], @@ -24939,95 +26431,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, + "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": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "800000", + "notionalFloor": "80000", "maintMarginRatio": "0.05", - "cum": "675.0" + "cum": "2150.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "42150.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "82150.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "4000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "332150.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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" } } ], @@ -25281,95 +26789,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, + "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": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "800000", + "notionalFloor": "80000", "maintMarginRatio": "0.05", - "cum": "675.0" + "cum": "2150.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "42150.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "82150.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "4000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "332150.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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" } } ], @@ -26357,14 +27881,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" } }, @@ -26372,96 +27896,112 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "2", - "initialLeverage": "15", - "notionalCap": "50000", + "initialLeverage": "25", + "notionalCap": "25000", "notionalFloor": "5000", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.02", "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 200000.0, + "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": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "200000", - "notionalFloor": "50000", + "notionalCap": "800000", + "notionalFloor": "80000", "maintMarginRatio": "0.05", - "cum": "1275.0" + "cum": "2150.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "1600000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "11275.0" + "cum": "42150.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "23775.0" + "cum": "82150.0" } }, { - "tier": 6.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": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "4000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "148775.0" + "cum": "332150.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "648775.0" + "cum": "1332150.0" } } ], @@ -26943,79 +28483,209 @@ "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 8.0, + "info": { + "bracket": "3", + "initialLeverage": "8", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 250000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "250000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 250000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "250000", + "maintMarginRatio": "0.125", + "cum": "11925.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "386925.0" + } + } + ], + "SAGA/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": "10", + "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": 8.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "3", - "initialLeverage": "8", - "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": 250000.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": "250000", - "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": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "5", "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11925.0" + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "26400.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 1250000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "1250000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "51400.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "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": "6", + "bracket": "8", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "2500000", "maintMarginRatio": "0.5", - "cum": "386925.0" + "cum": "832650.0" } } ], @@ -29239,95 +30909,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, + "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": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "800000", + "notionalFloor": "80000", "maintMarginRatio": "0.05", - "cum": "675.0" + "cum": "2150.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "42150.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "82150.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "4000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "332150.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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" } } ], @@ -30145,6 +31831,120 @@ } } ], + "TAO/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" + } + } + ], "THETA/USDT:USDT": [ { "tier": 1.0, @@ -30275,6 +32075,136 @@ } } ], + "TIA/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" + } + } + ], "TIA/USDT:USDT": [ { "tier": 1.0, @@ -30519,6 +32449,120 @@ } } ], + "TNSR/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" + } + } + ], "TOKEN/USDT:USDT": [ { "tier": 1.0, @@ -30753,95 +32797,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, + "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": "3", + "bracket": "4", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "800000", + "notionalFloor": "80000", "maintMarginRatio": "0.05", - "cum": "675.0" + "cum": "2150.0" } }, { - "tier": 4.0, + "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, + "minNotional": 800000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { - "bracket": "4", + "bracket": "5", "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "800000", "maintMarginRatio": "0.1", - "cum": "5675.0" + "cum": "42150.0" } }, { - "tier": 5.0, + "tier": 6.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 500000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { - "bracket": "5", + "bracket": "6", "initialLeverage": "4", - "notionalCap": "500000", - "notionalFloor": "200000", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "10675.0" + "cum": "82150.0" } }, { - "tier": 6.0, + "tier": 7.0, "currency": "USDT", - "minNotional": 500000.0, - "maxNotional": 1000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "500000", + "notionalCap": "4000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.25", - "cum": "73175.0" + "cum": "332150.0" } }, { - "tier": 7.0, + "tier": 8.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "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" } } ], @@ -32195,6 +34255,136 @@ } } ], + "W/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": 50000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "50000", + "notionalFloor": "25000", + "maintMarginRatio": "0.025", + "cum": "150.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "500000", + "notionalFloor": "50000", + "maintMarginRatio": "0.05", + "cum": "1400.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.1", + "cum": "26400.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 1250000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "1250000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.125", + "cum": "51400.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "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": "8", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "2500000", + "maintMarginRatio": "0.5", + "cum": "832650.0" + } + } + ], "WAVES/USDT:USDT": [ { "tier": 1.0, @@ -32423,10 +34613,10 @@ } } ], - "WIF/USDT:USDT": [ + "WIF/USDC:USDC": [ { "tier": 1.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.015, @@ -32442,7 +34632,7 @@ }, { "tier": 2.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.02, @@ -32458,7 +34648,7 @@ }, { "tier": 3.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 25000.0, "maxNotional": 80000.0, "maintenanceMarginRate": 0.025, @@ -32474,7 +34664,7 @@ }, { "tier": 4.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 80000.0, "maxNotional": 800000.0, "maintenanceMarginRate": 0.05, @@ -32490,7 +34680,7 @@ }, { "tier": 5.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 800000.0, "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, @@ -32506,7 +34696,7 @@ }, { "tier": 6.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 1600000.0, "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, @@ -32522,7 +34712,7 @@ }, { "tier": 7.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 2000000.0, "maxNotional": 4000000.0, "maintenanceMarginRate": 0.25, @@ -32538,7 +34728,7 @@ }, { "tier": 8.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 4000000.0, "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, @@ -32553,6 +34743,136 @@ } } ], + "WIF/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": 50000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "2", + "initialLeverage": "25", + "notionalCap": "50000", + "notionalFloor": "5000", + "maintMarginRatio": "0.02", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "3", + "initialLeverage": "20", + "notionalCap": "200000", + "notionalFloor": "50000", + "maintMarginRatio": "0.025", + "cum": "275.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "4", + "initialLeverage": "10", + "notionalCap": "2000000", + "notionalFloor": "200000", + "maintMarginRatio": "0.05", + "cum": "5275.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "5", + "initialLeverage": "5", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.1", + "cum": "105275.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "6", + "initialLeverage": "4", + "notionalCap": "5000000", + "notionalFloor": "4000000", + "maintMarginRatio": "0.125", + "cum": "205275.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 5000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "10000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.25", + "cum": "830275.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "8", + "initialLeverage": "1", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.5", + "cum": "3330275.0" + } + } + ], "WLD/USDC:USDC": [ { "tier": 1.0, diff --git a/freqtrade/exchange/bingx.py b/freqtrade/exchange/bingx.py new file mode 100644 index 00000000000..0bbf4a19d7a --- /dev/null +++ b/freqtrade/exchange/bingx.py @@ -0,0 +1,19 @@ +""" Bingx exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bingx(Exchange): + """ + Bingx exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 1000, + } diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 63047066aac..1891902f50c 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -99,7 +99,7 @@ def additional_exchange_init(self) -> None: logger.info("Bybit: Standard account.") except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' ) from e @@ -239,7 +239,7 @@ def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None return orders - def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: order = super().fetch_order(order_id, pair, params) if ( order.get('status') == 'canceled' diff --git a/freqtrade/exchange/check_exchange.py b/freqtrade/exchange/check_exchange.py index 69330bcd07a..94e330cc15a 100644 --- a/freqtrade/exchange/check_exchange.py +++ b/freqtrade/exchange/check_exchange.py @@ -56,7 +56,7 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool: logger.info(f'Exchange "{exchange}" is officially supported ' f'by the Freqtrade development team.') else: - logger.warning(f'Exchange "{exchange}" is known to the the ccxt library, ' + logger.warning(f'Exchange "{exchange}" is known to the ccxt library, ' f'available for the bot, but not officially supported ' f'by the Freqtrade development team. ' f'It may work flawlessly (please report back) or have serious issues. ' diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 428083f5ff2..eed852e7c36 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -44,7 +44,7 @@ safe_value_fallback2) from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.util import dt_from_ts, dt_now -from freqtrade.util.datetime_helpers import dt_humanize, dt_ts +from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts from freqtrade.util.periodic_cache import PeriodicCache @@ -239,8 +239,8 @@ def validate_config(self, config): self.validate_pricing(config['exit_pricing']) self.validate_pricing(config['entry_pricing']) - def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt, - ccxt_kwargs: Dict = {}) -> ccxt.Exchange: + def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt, *, + ccxt_kwargs: Dict) -> ccxt.Exchange: """ Initialize ccxt with given config and return valid ccxt instance. @@ -348,10 +348,13 @@ def ohlcv_candle_limit( return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get( timeframe, self._ft_has.get('ohlcv_candle_limit'))) - def get_markets(self, base_currencies: List[str] = [], quote_currencies: List[str] = [], - spot_only: bool = False, margin_only: bool = False, futures_only: bool = False, - tradable_only: bool = True, - active_only: bool = False) -> Dict[str, Any]: + def get_markets( + self, + base_currencies: Optional[List[str]] = None, + quote_currencies: Optional[List[str]] = None, + spot_only: bool = False, margin_only: bool = False, futures_only: bool = False, + tradable_only: bool = True, + active_only: bool = False) -> Dict[str, Any]: """ Return exchange ccxt markets, filtered out by base currency and quote currency if this was requested in parameters. @@ -758,7 +761,7 @@ def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = RO def price_get_one_pip(self, pair: str, price: float) -> float: """ - Get's the "1 pip" value for this pair. + Gets the "1 pip" value for this pair. Used in PriceFilter to calculate the 1pip movements. """ precision = self.markets[pair]['precision']['price'] @@ -848,7 +851,7 @@ def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: # Dry-run methods def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, leverage: float, params: Dict = {}, + rate: float, leverage: float, params: Optional[Dict] = None, stop_loss: bool = False) -> Dict[str, Any]: now = dt_now() order_id = f'dry_run_{side}_{pair}_{now.timestamp()}' @@ -1122,7 +1125,7 @@ def create_order( f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1259,7 +1262,7 @@ def create_stoploss(self, pair: str, amount: float, stop_price: float, order_typ f'stop-price {stop_price_norm}. Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f"Could not place stoploss order due to {e.__class__.__name__}. " f"Message: {e}") from e @@ -1290,16 +1293,18 @@ def fetch_order_emulated(self, order_id: str, pair: str, params: Dict) -> Dict: f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) - def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) + if params is None: + params = {} try: if not self.exchange_has('fetchOrder'): return self.fetch_order_emulated(order_id, pair, params) @@ -1315,13 +1320,13 @@ def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e - def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: return self.fetch_order(order_id, pair, params) def fetch_order_or_stoploss_order(self, order_id: str, pair: str, @@ -1347,7 +1352,7 @@ def check_order_canceled_empty(self, order: Dict) -> bool: and order.get('filled') == 0.0) @retrier - def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def cancel_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: if self._config['dry_run']: try: order = self.fetch_dry_run_order(order_id) @@ -1357,6 +1362,8 @@ def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: except InvalidOrderException: return {} + if params is None: + params = {} try: order = self._api.cancel_order(order_id, pair, params=params) self._log_exchange_response('cancel_order', order) @@ -1367,13 +1374,14 @@ def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: f'Could not cancel order. Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e - def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def cancel_stoploss_order( + self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: return self.cancel_order(order_id, pair, params) def is_cancel_order_result_suitable(self, corder) -> bool: @@ -1449,7 +1457,7 @@ def get_balances(self) -> dict: return balances except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1473,7 +1481,7 @@ def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]: return positions except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get positions due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1517,7 +1525,7 @@ def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None return orders except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not fetch positions due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1538,7 +1546,7 @@ def fetch_trading_fees(self) -> Dict[str, Any]: return trading_fees except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not fetch trading fees due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1569,7 +1577,7 @@ def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = Fa f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load bids/asks due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1606,7 +1614,7 @@ def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) raise TemporaryError from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1624,7 +1632,7 @@ def fetch_ticker(self, pair: str) -> Ticker: return data except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1665,7 +1673,7 @@ def fetch_l2_order_book(self, pair: str, limit: int = 100) -> OrderBook: f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1844,7 +1852,7 @@ def get_trades_for_order(self, order_id: str, pair: str, since: datetime, return matched_trades except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -1878,7 +1886,7 @@ def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1 price=price, takerOrMaker=taker_or_maker)['rate'] except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -2000,14 +2008,14 @@ async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, logger.debug( "one_call: %s msecs (%s)", one_call, - dt_humanize(dt_now() - timedelta(milliseconds=one_call), only_distance=True) + dt_humanize_delta(dt_now() - timedelta(milliseconds=one_call)) ) input_coroutines = [self._async_get_candle_history( pair, timeframe, candle_type, since) for since in range(since_ms, until_ms or dt_ts(), one_call)] data: List = [] - # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling + # Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling for input_coro in chunks(input_coroutines, 100): results = await asyncio.gather(*input_coro, return_exceptions=True) @@ -2124,7 +2132,7 @@ def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, Only used in the dataprovider.refresh() method. :param pair_list: List of 2 element tuples containing pair, interval to refresh :param since_ms: time since when to download, in milliseconds - :param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists + :param cache: Assign result to _klines. Useful for one-off downloads like for pairlists :param drop_incomplete: Control candle dropping. Specifying None defaults to _ohlcv_partial_candle :return: Dict of [{(pair, timeframe): Dataframe}] @@ -2135,7 +2143,7 @@ def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache) results_df = {} - # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling + # Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling for input_coro in chunks(input_coroutines, 100): async def gather_stuff(): return await asyncio.gather(*input_coro, return_exceptions=True) @@ -2262,7 +2270,7 @@ async def _async_get_candle_history( f'candle (OHLCV) data. Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not fetch historical candle (OHLCV) data ' f'for pair {pair} due to {e.__class__.__name__}. ' f'Message: {e}') from e @@ -2295,7 +2303,7 @@ async def _async_fetch_trades(self, pair: str, since: Optional[int] = None, params: Optional[dict] = None) -> Tuple[List[List], Any]: """ - Asyncronously gets trade history using fetch_trades. + Asynchronously gets trade history using fetch_trades. Handles exchange errors, does one call to the exchange. :param pair: Pair to fetch trade data for :param since: Since as integer timestamp in milliseconds @@ -2322,7 +2330,7 @@ async def _async_fetch_trades(self, pair: str, f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. ' f'Message: {e}') from e except ccxt.BaseError as e: @@ -2352,7 +2360,7 @@ async def _async_get_trade_history_id(self, pair: str, since: Optional[int] = None, from_id: Optional[str] = None) -> Tuple[str, List[List]]: """ - Asyncronously gets trade history using fetch_trades + Asynchronously gets trade history using fetch_trades use this when exchange uses id-based iteration (check `self._trades_pagination`) :param pair: Pair to fetch trade data for :param since: Since as integer timestamp in milliseconds @@ -2403,7 +2411,7 @@ async def _async_get_trade_history_id(self, pair: str, async def _async_get_trade_history_time(self, pair: str, until: int, since: Optional[int] = None) -> Tuple[str, List[List]]: """ - Asyncronously gets trade history using fetch_trades, + Asynchronously gets trade history using fetch_trades, when the exchange uses time-based iteration (check `self._trades_pagination`) :param pair: Pair to fetch trade data for :param since: Since as integer timestamp in milliseconds @@ -2521,7 +2529,7 @@ def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int] return sum(fee['amount'] for fee in funding_history) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -2533,7 +2541,7 @@ def get_leverage_tiers(self) -> Dict[str, List[Dict]]: return self._api.fetch_leverage_tiers() except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load leverage tiers due to {e.__class__.__name__}. Message: {e}' ) from e @@ -2548,7 +2556,7 @@ async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]] return symbol, tier except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load leverage tiers for {symbol}' f' due to {e.__class__.__name__}. Message: {e}' @@ -2762,7 +2770,7 @@ def _set_leverage( if not accept_fail: raise TemporaryError( f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: @@ -2786,7 +2794,7 @@ def funding_fee_cutoff(self, open_date: datetime) -> bool: @retrier def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False, - params: dict = {}): + params: Optional[Dict] = None): """ Set's the margin mode on the exchange to cross or isolated for a specific pair :param pair: base/quote currency pair (e.g. "ADA/USDT") @@ -2795,6 +2803,8 @@ def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool # Some exchanges only support one margin_mode type return + if params is None: + params = {} try: res = self._api.set_margin_mode(margin_mode.value, pair, params) self._log_exchange_response('set_margin_mode', res) @@ -2804,7 +2814,7 @@ def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool if not accept_fail: raise TemporaryError( f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index cb749cb6622..1d25e2df31a 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -79,7 +79,7 @@ def get_trades_for_order(self, order_id: str, pair: str, since: datetime, # As such, futures orders on gate will not contain a fee, which causes # a repeated "update fee" cycle and wrong calculations. # Therefore we patch the response with fees if it's not available. - # An alternative also contianing fees would be + # An alternative also containing fees would be # privateFuturesGetSettleAccountBook({"settle": "usdt"}) pair_fees = self._trading_fees.get(pair, {}) if pair_fees: @@ -98,7 +98,7 @@ def get_trades_for_order(self, order_id: str, pair: str, since: datetime, def get_order_id_conditional(self, order: Dict[str, Any]) -> str: return safe_value_fallback2(order, order, 'id_stop', 'id') - def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: order = self.fetch_order( order_id=order_id, pair=pair, @@ -119,7 +119,8 @@ def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> D return order1 return order - def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def cancel_stoploss_order( + self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: return self.cancel_order( order_id=order_id, pair=pair, diff --git a/freqtrade/exchange/idex.py b/freqtrade/exchange/idex.py new file mode 100644 index 00000000000..eae5ad15572 --- /dev/null +++ b/freqtrade/exchange/idex.py @@ -0,0 +1,19 @@ +""" Idex exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Idex(Exchange): + """ + Idex exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 1000, + } diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index c8fecdd10d3..f30d79cba9f 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -84,7 +84,7 @@ def get_balances(self) -> dict: return balances except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 783a197d279..d919a73cc9e 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -56,7 +56,7 @@ def ohlcv_candle_limit( """ Exchange ohlcv candle limit OKX has the following behaviour: - * 300 candles for uptodate data + * 300 candles for up-to-date data * 100 candles for historic data * 100 candles for additional candles (not futures or spot). :param timeframe: Timeframe to check @@ -87,7 +87,7 @@ def additional_exchange_init(self) -> None: self.net_only = accounts[0].get('info', {}).get('posMode') == 'net_mode' except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' ) from e @@ -153,7 +153,7 @@ def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: + except (ccxt.OperationFailed, ccxt.ExchangeError) as e: already_set = self.__fetch_leverage_already_set(pair, leverage, side) if not already_set: raise TemporaryError( @@ -202,7 +202,7 @@ def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict: order['type'] = 'stoploss' return order - def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) @@ -232,7 +232,8 @@ def get_order_id_conditional(self, order: Dict[str, Any]) -> str: return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] - def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + def cancel_stoploss_order( + self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: params1 = {'stop': True} # 'ordType': 'conditional' # diff --git a/freqtrade/freqai/RL/BaseEnvironment.py b/freqtrade/freqai/RL/BaseEnvironment.py index b8548dd1612..f53ab9d2752 100644 --- a/freqtrade/freqai/RL/BaseEnvironment.py +++ b/freqtrade/freqai/RL/BaseEnvironment.py @@ -222,7 +222,7 @@ def reset(self, seed=None): @abstractmethod def step(self, action: int): """ - Step depeneds on action types, this must be inherited. + Step depends on action types, this must be inherited. """ return @@ -326,7 +326,7 @@ def calculate_reward(self, action: int) -> float: def _update_unrealized_total_profit(self): """ - Update the unrealized total profit incase of episode end. + Update the unrealized total profit in case of episode end. """ if self._position in (Positions.Long, Positions.Short): pnl = self.get_unrealized_profit() @@ -357,7 +357,7 @@ def get_actions(self) -> Type[Enum]: """ return self.actions - # Keeping around incase we want to start building more complex environment + # Keeping around in case we want to start building more complex environment # templates in the future. # def most_recent_return(self): # """ diff --git a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py index 5b98efb49ae..71fd9c28c65 100644 --- a/freqtrade/freqai/RL/BaseReinforcementLearningModel.py +++ b/freqtrade/freqai/RL/BaseReinforcementLearningModel.py @@ -311,7 +311,7 @@ def build_ohlc_price_dataframes(self, data_dictionary: dict, if not prices_train_old.empty: prices_train = prices_train_old rename_dict = rename_dict_old - logger.warning('Reinforcement learning module didnt find the correct raw prices ' + logger.warning('Reinforcement learning module didn\'t find the correct raw prices ' 'assigned in feature_engineering_standard(). ' 'Please assign them with:\n' 'dataframe["%-raw_close"] = dataframe["close"]\n' @@ -458,7 +458,7 @@ def make_env(MyRLEnv: Type[BaseEnvironment], env_id: str, rank: int, :param env_id: (str) the environment ID :param num_env: (int) the number of environment you wish to have in subprocesses - :param seed: (int) the inital seed for RNG + :param seed: (int) the initial seed for RNG :param rank: (int) index of the subprocess :param env_info: (dict) all required arguments to instantiate the environment. :return: (Callable) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 6316c0a86e6..77462f3118d 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -4,6 +4,7 @@ import re import shutil import threading +import warnings from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, Tuple, TypedDict @@ -262,10 +263,11 @@ def set_pair_dict_info(self, metadata: dict) -> None: self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy() return - def set_initial_return_values(self, pair: str, - pred_df: DataFrame, - dataframe: DataFrame - ) -> None: + def set_initial_return_values( + self, pair: str, + pred_df: DataFrame, + dataframe: DataFrame + ) -> None: """ Set the initial return values to the historical predictions dataframe. This avoids needing to repredict on historical candles, and also stores historical predictions despite @@ -278,10 +280,15 @@ def set_initial_return_values(self, pair: str, new_pred = pred_df.copy() # set new_pred values to nans (we want to signal to user that there was nothing - # historically made during downtime. The newest pred will get appeneded later in + # historically made during downtime. The newest pred will get appended later in # append_model_predictions) - new_pred.iloc[:, :] = np.nan + new_pred["date_pred"] = dataframe["date"] + # set everything to nan except date_pred + columns_to_nan = new_pred.columns.difference(['date_pred', 'date']) + new_pred[columns_to_nan] = new_pred[columns_to_nan].astype( + float).values * np.nan + hist_preds = self.historic_predictions[pair].copy() # ensure both dataframes have the same date format so they can be merged @@ -290,7 +297,8 @@ def set_initial_return_values(self, pair: str, # find the closest common date between new_pred and historic predictions # and cut off the new_pred dataframe at that date - common_dates = pd.merge(new_pred, hist_preds, on="date_pred", how="inner") + common_dates = pd.merge(new_pred, hist_preds, + on="date_pred", how="inner") if len(common_dates.index) > 0: new_pred = new_pred.iloc[len(common_dates):] else: @@ -298,15 +306,23 @@ def set_initial_return_values(self, pair: str, "predictions. You likely left your FreqAI instance offline " f"for more than {len(dataframe.index)} candles.") - # reindex new_pred columns to match the historic predictions dataframe - new_pred_reindexed = new_pred.reindex(columns=hist_preds.columns) - df_concat = pd.concat([hist_preds, new_pred_reindexed], ignore_index=True) + # Pandas warns that its keeping dtypes of non NaN columns... + # yea we know and we already want that behavior. Ignoring. + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + # reindex new_pred columns to match the historic predictions dataframe + new_pred_reindexed = new_pred.reindex(columns=hist_preds.columns) + df_concat = pd.concat( + [hist_preds, new_pred_reindexed], + ignore_index=True + ) # any missing values will get zeroed out so users can see the exact # downtime in FreqUI df_concat = df_concat.fillna(0) self.historic_predictions[pair] = df_concat - self.model_return_values[pair] = df_concat.tail(len(dataframe.index)).reset_index(drop=True) + self.model_return_values[pair] = df_concat.tail( + len(dataframe.index)).reset_index(drop=True) def append_model_predictions(self, pair: str, predictions: DataFrame, do_preds: NDArray[np.int_], @@ -323,38 +339,56 @@ def append_model_predictions(self, pair: str, predictions: DataFrame, index = self.historic_predictions[pair].index[-1:] columns = self.historic_predictions[pair].columns - zeros_df = pd.DataFrame(np.zeros((1, len(columns))), index=index, columns=columns) + zeros_df = pd.DataFrame( + np.zeros((1, len(columns))), + index=index, + columns=columns + ) self.historic_predictions[pair] = pd.concat( - [self.historic_predictions[pair], zeros_df], ignore_index=True, axis=0) + [self.historic_predictions[pair], zeros_df], + ignore_index=True, + axis=0 + ) df = self.historic_predictions[pair] # model outputs and associated statistics for label in predictions.columns: - df[label].iloc[-1] = predictions[label].iloc[-1] + label_loc = df.columns.get_loc(label) + pred_label_loc = predictions.columns.get_loc(label) + df.iloc[-1, label_loc] = predictions.iloc[-1, pred_label_loc] if df[label].dtype == object: continue - df[f"{label}_mean"].iloc[-1] = dk.data["labels_mean"][label] - df[f"{label}_std"].iloc[-1] = dk.data["labels_std"][label] + label_mean_loc = df.columns.get_loc(f"{label}_mean") + label_std_loc = df.columns.get_loc(f"{label}_std") + df.iloc[-1, label_mean_loc] = dk.data["labels_mean"][label] + df.iloc[-1, label_std_loc] = dk.data["labels_std"][label] # outlier indicators - df["do_predict"].iloc[-1] = do_preds[-1] + do_predict_loc = df.columns.get_loc("do_predict") + df.iloc[-1, do_predict_loc] = do_preds[-1] if self.freqai_info["feature_parameters"].get("DI_threshold", 0) > 0: - df["DI_values"].iloc[-1] = dk.DI_values[-1] + DI_values_loc = df.columns.get_loc("DI_values") + df.iloc[-1, DI_values_loc] = dk.DI_values[-1] # extra values the user added within custom prediction model if dk.data['extra_returns_per_train']: rets = dk.data['extra_returns_per_train'] for return_str in rets: - df[return_str].iloc[-1] = rets[return_str] - - # this logic carries users between version without needing to - # change their identifier - if 'close_price' not in df.columns: - df['close_price'] = np.nan - df['date_pred'] = np.nan - - df['close_price'].iloc[-1] = strat_df['close'].iloc[-1] - df['date_pred'].iloc[-1] = strat_df['date'].iloc[-1] + return_loc = df.columns.get_loc(return_str) + df.iloc[-1, return_loc] = rets[return_str] + + high_price_loc = df.columns.get_loc("high_price") + high_loc = strat_df.columns.get_loc("high") + df.iloc[-1, high_price_loc] = strat_df.iloc[-1, high_loc] + low_price_loc = df.columns.get_loc("low_price") + low_loc = strat_df.columns.get_loc("low") + df.iloc[-1, low_price_loc] = strat_df.iloc[-1, low_loc] + close_price_loc = df.columns.get_loc("close_price") + close_loc = strat_df.columns.get_loc("close") + df.iloc[-1, close_price_loc] = strat_df.iloc[-1, close_loc] + date_pred_loc = df.columns.get_loc("date_pred") + date_loc = strat_df.columns.get_loc("date") + df.iloc[-1, date_pred_loc] = strat_df.iloc[-1, date_loc] self.model_return_values[pair] = df.tail(len_df).reset_index(drop=True) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 4c30ce69091..83439939009 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -24,6 +24,8 @@ from freqtrade.strategy.interface import IStrategy +pd.set_option('future.no_silent_downcasting', True) + SECONDS_IN_DAY = 86400 SECONDS_IN_HOUR = 3600 @@ -221,7 +223,7 @@ def filter_features( filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan) drop_index = pd.isnull(filtered_df).any(axis=1) # get the rows that have NaNs, - drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement. + drop_index = drop_index.replace(True, 1).replace(False, 0).infer_objects(copy=False) if (training_filter): # we don't care about total row number (total no. datapoints) in training, we only care @@ -229,7 +231,9 @@ def filter_features( # if labels has multiple columns (user wants to train multiple modelEs), we detect here labels = unfiltered_df.filter(label_list, axis=1) drop_index_labels = pd.isnull(labels).any(axis=1) - drop_index_labels = drop_index_labels.replace(True, 1).replace(False, 0) + drop_index_labels = drop_index_labels.replace( + True, 1 + ).replace(False, 0).infer_objects(copy=False) dates = unfiltered_df['date'] filtered_df = filtered_df[ (drop_index == 0) & (drop_index_labels == 0) @@ -608,7 +612,7 @@ def extract_corr_pair_columns_from_populated_indicators( pairs = self.freqai_config["feature_parameters"].get("include_corr_pairlist", []) for pair in pairs: - pair = pair.replace(':', '') # lightgbm doesnt like colons + pair = pair.replace(':', '') # lightgbm does not like colons pair_cols = [col for col in dataframe.columns if col.startswith("%") and f"{pair}_" in col] @@ -634,7 +638,7 @@ def attach_corr_pair_columns(self, dataframe: DataFrame, pairs = self.freqai_config["feature_parameters"].get("include_corr_pairlist", []) current_pair = current_pair.replace(':', '') for pair in pairs: - pair = pair.replace(':', '') # lightgbm doesnt work with colons + pair = pair.replace(':', '') # lightgbm does not work with colons if current_pair != pair: dataframe = dataframe.merge(corr_dataframes[pair], how='left', on='date') @@ -837,7 +841,7 @@ def fit_labels(self) -> None: f = spy.stats.norm.fit(self.data_dictionary["train_labels"][label]) self.data["labels_mean"][label], self.data["labels_std"][label] = f[0], f[1] - # incase targets are classifications + # in case targets are classifications for label in self.unique_class_list: self.data["labels_mean"][label], self.data["labels_std"][label] = 0, 0 diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index d9bc8d3a40b..f68b45b99c8 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -222,7 +222,7 @@ def _start_scanning(self, strategy: IStrategy) -> None: time.sleep(1) pair = self.train_queue[0] - # ensure pair is avaialble in dp + # ensure pair is available in dp if pair not in strategy.dp.current_whitelist(): self.train_queue.popleft() logger.warning(f'{pair} not in current whitelist, removing from train queue.') @@ -675,6 +675,8 @@ def set_initial_historic_predictions( for return_str in dk.data['extra_returns_per_train']: hist_preds_df[return_str] = dk.data['extra_returns_per_train'][return_str] + hist_preds_df['high_price'] = strat_df['high'] + hist_preds_df['low_price'] = strat_df['low'] hist_preds_df['close_price'] = strat_df['close'] hist_preds_df['date_pred'] = strat_df['date'] @@ -716,9 +718,6 @@ def inference_timer(self, do: Literal['start', 'stop'] = 'start', pair: str = '' if self.pair_it == self.total_pairs: logger.info( f'Total time spent inferencing pairlist {self.inference_time:.2f} seconds') - if self.inference_time > 0.25 * self.base_tf_seconds: - logger.warning("Inference took over 25% of the candle time. Reduce pairlist to" - " avoid blinding open trades and degrading performance.") self.pair_it = 0 self.inference_time = 0 return diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py index 9aabdf7ad67..a03a0c742e9 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -74,7 +74,7 @@ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) criterion = torch.nn.CrossEntropyLoss() - # check if continual_learning is activated, and retreive the model to continue training + # check if continual_learning is activated, and retrieve the model to continue training trainer = self.get_init_model(dk.pair) if trainer is None: trainer = PyTorchModelTrainer( diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py index dc8dc4b610a..ec5c0ba811f 100644 --- a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -69,7 +69,7 @@ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) criterion = torch.nn.MSELoss() - # check if continual_learning is activated, and retreive the model to continue training + # check if continual_learning is activated, and retrieve the model to continue training trainer = self.get_init_model(dk.pair) if trainer is None: trainer = PyTorchModelTrainer( diff --git a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py index b1f2eecc645..8f245ed834e 100644 --- a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py @@ -80,7 +80,7 @@ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: model.to(self.device) optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) criterion = torch.nn.MSELoss() - # check if continual_learning is activated, and retreive the model to continue training + # check if continual_learning is activated, and retrieve the model to continue training trainer = self.get_init_model(dk.pair) if trainer is None: trainer = PyTorchTransformerTrainer( diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py index f014da60265..3fab83cff4b 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner_multiproc.py @@ -63,6 +63,6 @@ def set_train_and_eval_environments(self, data_dictionary: Dict[str, Any], is_masking_supported(self.eval_env))) # TENSORBOARD CALLBACK DOES NOT RECOMMENDED TO USE WITH MULTIPLE ENVS, - # IT WILL RETURN FALSE INFORMATIONS, NEVERTHLESS NOT THREAD SAFE WITH SB3!!! + # IT WILL RETURN FALSE INFORMATION, NEVERTHELESS NOT THREAD SAFE WITH SB3!!! actions = self.train_env.env_method("get_actions")[0] self.tensorboard_callback = TensorboardCallback(verbose=1, actions=actions) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py index a91513dcb66..5c1db3c6575 100644 --- a/freqtrade/freqai/torch/PyTorchModelTrainer.py +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -38,7 +38,7 @@ def __init__( :param init_model: A dictionary containing the initial model/optimizer state_dict and model_meta_data saved by self.save() method. :param model_meta_data: Additional metadata about the model (optional). - :param data_convertor: convertor from pd.DataFrame to torch.tensor. + :param data_convertor: converter from pd.DataFrame to torch.tensor. :param n_steps: used to calculate n_epochs. The number of training iterations to run. iteration here refers to the number of times optimizer.step() is called. ignored if n_epochs is set. diff --git a/freqtrade/freqai/utils.py b/freqtrade/freqai/utils.py index 22d75bc16d3..8ac175e4d9e 100644 --- a/freqtrade/freqai/utils.py +++ b/freqtrade/freqai/utils.py @@ -178,7 +178,7 @@ def record_params(config: Dict[str, Any], full_path: Path) -> None: def get_timerange_backtest_live_models(config: Config) -> str: """ - Returns a formated timerange for backtest live/ready models + Returns a formatted timerange for backtest live/ready models :param config: Configuration dictionary :return: a string timerange (format example: '20220801-20220822') diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0184741ffa3..810ce0ae545 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -37,6 +37,7 @@ RPCExitMsg, RPCProtectionMsg) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.util import MeasureTime from freqtrade.util.migrations import migrate_binance_futures_names from freqtrade.wallets import Wallets @@ -64,7 +65,7 @@ def __init__(self, config: Config) -> None: # Init objects self.config = config exchange_config: ExchangeConfig = deepcopy(config['exchange']) - # Remove credentials from original exchange config to avoid accidental credentail exposure + # Remove credentials from original exchange config to avoid accidental credential exposure remove_exchange_credentials(config['exchange'], True) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) @@ -117,7 +118,8 @@ def __init__(self, config: Config) -> None: # Protect exit-logic from forcesell and vice versa self._exit_lock = Lock() - LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) + timeframe_secs = timeframe_to_seconds(self.strategy.timeframe) + LoggingMixin.__init__(self, logger, timeframe_secs) self._schedule = Scheduler() @@ -139,6 +141,16 @@ def update(): # Initialize protections AFTER bot start - otherwise parameters are not loaded. self.protections = ProtectionManager(self.config, self.strategy.protections) + def log_took_too_long(duration: float, time_limit: float): + logger.warning( + f"Strategy analysis took {duration:.2f}, which is 25% of the timeframe. " + "This can lead to delayed orders and missed signals." + "Consider either reducing the amount of work your strategy performs " + "or reduce the amount of pairs in the Pairlist." + ) + + self._measure_execution = MeasureTime(log_took_too_long, timeframe_secs * 0.25) + def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None: """ Public method for users of this class (worker, etc.) to send notifications @@ -175,7 +187,7 @@ def cleanup(self) -> None: try: Trade.commit() except Exception: - # Exeptions here will be happening if the db disappeared. + # Exceptions here will be happening if the db disappeared. # At which point we can no longer commit anyway. pass @@ -223,10 +235,11 @@ def process(self) -> None: strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)( current_time=datetime.now(timezone.utc)) - self.strategy.analyze(self.active_pair_whitelist) + with self._measure_execution: + self.strategy.analyze(self.active_pair_whitelist) with self._exit_lock: - # Check for exchange cancelations, timeouts and user requested replace + # Check for exchange cancellations, timeouts and user requested replace self.manage_open_orders() # Protect from collisions with force_exit. @@ -237,12 +250,12 @@ def process(self) -> None: # First process current opened trades (positions) self.exit_positions(trades) - # Check if we need to adjust our current positions before attempting to buy new trades. + # Check if we need to adjust our current positions before attempting to enter new trades. if self.strategy.position_adjustment_enable: with self._exit_lock: self.process_open_trade_positions() - # Then looking for buy opportunities + # Then looking for entry opportunities if self.get_free_open_trades(): self.enter_positions() if self.trading_mode == TradingMode.FUTURES: @@ -277,7 +290,7 @@ def check_for_open_trades(self): } self.rpc.send_msg(msg) - def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: + def _refresh_active_whitelist(self, trades: Optional[List[Trade]] = None) -> List[str]: """ Refresh active whitelist from pairlist or edge and extend it with pairs that have open trades. @@ -449,6 +462,7 @@ def handle_onexchange_order(self, trade: Trade): trade.pair, trade.open_date_utc - timedelta(seconds=10)) prev_exit_reason = trade.exit_reason prev_trade_state = trade.is_open + prev_trade_amount = trade.amount for order in orders: trade_order = [o for o in trade.orders if o.order_id == order['id']] @@ -480,6 +494,26 @@ def handle_onexchange_order(self, trade: Trade): send_msg=prev_trade_state != trade.is_open) else: trade.exit_reason = prev_exit_reason + total = self.wallets.get_total(trade.base_currency) if trade.base_currency else 0 + if total < trade.amount: + if total > trade.amount * 0.98: + logger.warning( + f"{trade} has a total of {trade.amount} {trade.base_currency}, " + f"but the Wallet shows a total of {total} {trade.base_currency}. " + f"Adjusting trade amount to {total}." + "This may however lead to further issues." + ) + trade.amount = total + else: + logger.warning( + f"{trade} has a total of {trade.amount} {trade.base_currency}, " + f"but the Wallet shows a total of {total} {trade.base_currency}. " + "Refusing to adjust as the difference is too large." + "This may however lead to further issues." + ) + if prev_trade_amount != trade.amount: + # Cancel stoploss on exchange if the amount changed + trade = self.cancel_stoploss_on_exchange(trade) Trade.commit() except ExchangeError: @@ -488,7 +522,7 @@ def handle_onexchange_order(self, trade: Trade): # catching https://github.com/freqtrade/freqtrade/issues/9025 logger.warning("Error finding onexchange order", exc_info=True) # -# BUY / enter positions / open trades logic and methods +# enter positions / open trades logic and methods # def enter_positions(self) -> int: @@ -538,10 +572,10 @@ def enter_positions(self) -> int: def create_trade(self, pair: str) -> bool: """ - Check the implemented trading strategy for buy signals. + Check the implemented trading strategy for entry signals. - If the pair triggers the buy signal a new trade record gets created - and the buy-order opening the trade gets issued towards the exchange. + If the pair triggers the enter signal a new trade record gets created + and the entry-order opening the trade gets issued towards the exchange. :return: True if a trade has been created. """ @@ -600,7 +634,7 @@ def create_trade(self, pair: str) -> bool: return False # -# BUY / increase positions / DCA logic and methods +# Modify positions / DCA logic and methods # def process_open_trade_positions(self): """ @@ -683,7 +717,7 @@ def check_and_call_adjust_trade_position(self, trade: Trade): def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool: """ - Checks depth of market before executing a buy + Checks depth of market before executing an entry """ conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0) logger.info(f"Checking depth of market for {pair} ...") @@ -727,10 +761,10 @@ def execute_entry( leverage_: Optional[float] = None, ) -> bool: """ - Executes a limit buy for the given pair - :param pair: pair for which we want to create a LIMIT_BUY + Executes an entry for the given pair + :param pair: pair for which we want to create a LIMIT order :param stake_amount: amount of stake-currency for the pair - :return: True if a buy order is created, false if it fails. + :return: True if an entry order is created, False if it fails. :raise: DependencyException or it's subclasses like ExchangeError. """ time_in_force = self.strategy.order_time_in_force['entry'] @@ -859,7 +893,7 @@ def execute_entry( trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True) else: - # This is additional buy, we reset fee_open_currency so timeout checking can work + # This is additional entry, we reset fee_open_currency so timeout checking can work trade.is_open = True trade.fee_open_currency = None trade.open_rate_requested = enter_limit_requested @@ -1232,7 +1266,7 @@ def handle_stoploss_on_exchange(self, trade: Trade) -> bool: return True if trade.has_open_orders or not trade.is_open: - # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case + # Trade has an open order, Stoploss-handling can't happen in this case # as the Amount on the exchange is tied up in another trade. # The trade can be closed already (sell-order fill confirmation came in this iteration) return False @@ -1290,12 +1324,12 @@ def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> Non def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: List[Dict]): """ - Perform required actions acording to existing stoploss orders of trade + Perform required actions according to existing stoploss orders of trade :param trade: Corresponding Trade :param stoploss_orders: Current on exchange stoploss orders :return: None """ - # If all stoploss orderd are canceled for some reason we add it again + # If all stoploss ordered are canceled for some reason we add it again canceled_sl_orders = [o for o in stoploss_orders if o['status'] in ('canceled', 'cancelled')] if ( @@ -1935,21 +1969,23 @@ def update_trade_state( trade.update_trade(order_obj, not send_msg) - trade = self._update_trade_after_fill(trade, order_obj) + trade = self._update_trade_after_fill(trade, order_obj, send_msg) Trade.commit() self.order_close_notify(trade, order_obj, stoploss_order, send_msg) return False - def _update_trade_after_fill(self, trade: Trade, order: Order) -> Trade: + def _update_trade_after_fill(self, trade: Trade, order: Order, send_msg: bool) -> 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) + if send_msg: + # Don't cancel stoploss in recovery modes immediately + trade = self.cancel_stoploss_on_exchange(trade) if not self.edge: # TODO: should shorting/leverage be supported by Edge, # then this will need to be fixed. @@ -1999,7 +2035,7 @@ def order_close_notify( self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade) def handle_protections(self, pair: str, side: LongShort) -> None: - # Lock pair for one candle to prevent immediate rebuys + # Lock pair for one candle to prevent immediate re-entries self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock') prot_trig = self.protections.stop_per_pair(pair, side=side) if prot_trig: @@ -2035,7 +2071,7 @@ def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, amount_ = trade.amount - amount if trade.nr_of_successful_entries >= 1 and order_obj.ft_order_side == trade.entry_side: - # In case of rebuy's, trade.amount doesn't contain the amount of the last entry. + # In case of re-entry's, trade.amount doesn't contain the amount of the last entry. amount_ = trade.amount + amount if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount_: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 326c48c62ed..1952c4fe73f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -19,6 +19,7 @@ from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider +from freqtrade.data.metrics import combined_dataframes_with_rel_mean from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType, RunMode, TradingMode) from freqtrade.exceptions import DependencyException, OperationalException @@ -296,7 +297,7 @@ def load_bt_data_detail(self) -> None: candle_type=CandleType.FUNDING_RATE ) - # For simplicity, assign to CandleType.Mark (might contian index candles!) + # For simplicity, assign to CandleType.Mark (might contain index candles!) mark_rates_dict = history.load_data( datadir=self.config['datadir'], pairs=self.pairlists.whitelist, @@ -565,7 +566,8 @@ def _get_adjust_trade_entry_for_candle( if stake_amount is not None and stake_amount < 0.0: amount = amount_to_contract_precision( - abs(stake_amount * trade.leverage) / current_rate, trade.amount_precision, + abs(stake_amount * trade.amount / trade.stake_amount), + trade.amount_precision, self.precision_mode, trade.contract_size) if amount == 0.0: return trade @@ -1215,7 +1217,7 @@ def backtest(self, processed: Dict, :return: DataFrame with trades (results of backtesting) """ self.prepare_backtest(self.enable_protections) - # Ensure wallets are uptodate (important for --strategy-list) + # Ensure wallets are up-to-date (important for --strategy-list) self.wallets.update() # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) @@ -1392,9 +1394,8 @@ def load_prior_backtest(self): def start(self) -> None: """ Run backtesting end-to-end - :return: None """ - data: Dict[str, Any] = {} + data: Dict[str, DataFrame] = {} data, timerange = self.load_bt_data() self.load_bt_data_detail() @@ -1421,7 +1422,9 @@ def start(self) -> None: self.results = results dt_appendix = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") if self.config.get('export', 'none') in ('trades', 'signals'): - store_backtest_stats(self.config['exportfilename'], self.results, dt_appendix) + combined_res = combined_dataframes_with_rel_mean(data, min_date, max_date) + store_backtest_stats(self.config['exportfilename'], self.results, dt_appendix, + market_change_data=combined_res) if (self.config.get('export', 'none') == 'signals' and self.dataprovider.runmode == RunMode.BACKTEST): diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 763fed7473d..5a09d92b540 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -237,8 +237,10 @@ def _params_update_for_json(result_dict, params, non_optimized, space: str) -> N result_dict.update(all_space_params) @staticmethod - def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None: - if space in params or space in non_optimized: + def _params_pretty_print( + params, space: str, header: str, non_optimized: Optional[Dict] = None) -> None: + + if space in params or (non_optimized and space in non_optimized): space_params = HyperoptTools._space_params(params, space, 5) no_params = HyperoptTools._space_params(non_optimized, space, 5) appendix = '' diff --git a/freqtrade/optimize/optimize_reports/__init__.py b/freqtrade/optimize/optimize_reports/__init__.py index 9e3ac46bc99..bb91bf33cf6 100644 --- a/freqtrade/optimize/optimize_reports/__init__.py +++ b/freqtrade/optimize/optimize_reports/__init__.py @@ -6,13 +6,12 @@ show_sorted_pairlist, text_table_add_metrics, text_table_bt_results, - text_table_exit_reason, text_table_periodic_breakdown, text_table_strategy, text_table_tags) from freqtrade.optimize.optimize_reports.bt_storage import (store_backtest_analysis_results, store_backtest_stats) from freqtrade.optimize.optimize_reports.optimize_reports import ( generate_all_periodic_breakdown_stats, generate_backtest_stats, generate_daily_stats, - generate_exit_reason_stats, generate_pair_metrics, generate_periodic_breakdown_stats, - generate_rejected_signals, generate_strategy_comparison, generate_strategy_stats, - generate_tag_metrics, generate_trade_signal_candles, generate_trading_stats) + generate_pair_metrics, generate_periodic_breakdown_stats, generate_rejected_signals, + generate_strategy_comparison, generate_strategy_stats, generate_tag_metrics, + generate_trade_signal_candles, generate_trading_stats) diff --git a/freqtrade/optimize/optimize_reports/bt_output.py b/freqtrade/optimize/optimize_reports/bt_output.py index 2a4be7e7a4a..f90a35469d0 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -60,32 +60,6 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") -def text_table_exit_reason(exit_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: - """ - Generate small table outlining Backtest results - :param exit_reason_stats: Exit reason metrics - :param stake_currency: Stakecurrency used - :return: pretty printed table with tabulate as string - """ - headers = [ - 'Exit Reason', - 'Exits', - 'Win Draws Loss Win%', - 'Avg Profit %', - f'Tot Profit {stake_currency}', - 'Tot Profit %', - ] - - 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'], - fmt_coin(t['profit_total_abs'], stake_currency, False), - t['profit_total_pct'], - ] for t in exit_reason_stats] - return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") - - def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_currency: str) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe @@ -93,20 +67,23 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr :param stake_currency: stake-currency - used to correctly name headers :return: pretty printed table with tabulate as string """ + fallback: str = '' if (tag_type == "enter_tag"): headers = _get_line_header("TAG", stake_currency) else: - headers = _get_line_header("TAG", stake_currency, 'Exits') + headers = _get_line_header("Exit Reason", stake_currency, 'Exits') + fallback = 'exit_reason' + floatfmt = _get_line_floatfmt(stake_currency) output = [ [ - t['key'] if t['key'] is not None and len( - t['key']) > 0 else "OTHER", + t['key'] if t.get('key') is not None and len( + str(t['key'])) > 0 else t.get(fallback, "OTHER"), t['trades'], t['profit_mean_pct'], t['profit_total_abs'], t['profit_total_pct'], - t['duration_avg'], + t.get('duration_avg'), generate_wins_draws_losses( t['wins'], t['draws'], @@ -301,7 +278,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: str, - backtest_breakdown=[]): + backtest_breakdown: List[str]): """ Print results for one strategy """ @@ -317,17 +294,16 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) - if (results.get('results_per_enter_tag') is not None): - table = text_table_tags("enter_tag", results['results_per_enter_tag'], stake_currency) + if (enter_tags := results.get('results_per_enter_tag')) is not None: + table = text_table_tags("enter_tag", enter_tags, stake_currency) if isinstance(table, str) and len(table) > 0: print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '=')) print(table) - exit_reasons = results.get('exit_reason_summary') - if exit_reasons: - table = text_table_exit_reason(exit_reason_stats=exit_reasons, - stake_currency=stake_currency) + if (exit_reasons := results.get('exit_reason_summary')) is not None: + table = text_table_tags("exit_tag", exit_reasons, stake_currency) + if isinstance(table, str) and len(table) > 0: print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) diff --git a/freqtrade/optimize/optimize_reports/bt_storage.py b/freqtrade/optimize/optimize_reports/bt_storage.py index 6b50412b348..a8a8bf7f263 100644 --- a/freqtrade/optimize/optimize_reports/bt_storage.py +++ b/freqtrade/optimize/optimize_reports/bt_storage.py @@ -1,6 +1,8 @@ import logging from pathlib import Path -from typing import Dict +from typing import Dict, Optional + +from pandas import DataFrame from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import file_dump_joblib, file_dump_json @@ -11,8 +13,26 @@ logger = logging.getLogger(__name__) +def _generate_filename(recordfilename: Path, appendix: str, suffix: str) -> Path: + """ + Generates a filename based on the provided parameters. + :param recordfilename: Path object, which can either be a filename or a directory. + :param appendix: use for the filename. e.g. backtest-result- + :param suffix: Suffix to use for the file, e.g. .json, .pkl + :return: Generated filename as a Path object + """ + if recordfilename.is_dir(): + filename = (recordfilename / f'backtest-result-{appendix}').with_suffix(suffix) + else: + filename = Path.joinpath( + recordfilename.parent, f'{recordfilename.stem}-{appendix}' + ).with_suffix(suffix) + return filename + + def store_backtest_stats( - recordfilename: Path, stats: BacktestResultType, dtappendix: str) -> Path: + recordfilename: Path, stats: BacktestResultType, dtappendix: str, *, + market_change_data: Optional[DataFrame] = None) -> Path: """ Stores backtest results :param recordfilename: Path object, which can either be a filename or a directory. @@ -21,12 +41,7 @@ def store_backtest_stats( :param stats: Dataframe containing the backtesting statistics :param dtappendix: Datetime to use for the filename """ - if recordfilename.is_dir(): - filename = (recordfilename / f'backtest-result-{dtappendix}.json') - else: - filename = Path.joinpath( - recordfilename.parent, f'{recordfilename.stem}-{dtappendix}' - ).with_suffix(recordfilename.suffix) + filename = _generate_filename(recordfilename, dtappendix, '.json') # Store metadata separately. file_dump_json(get_backtest_metadata_filename(filename), stats['metadata']) @@ -41,6 +56,11 @@ def store_backtest_stats( latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) + if market_change_data is not None: + filename_mc = _generate_filename(recordfilename, f"{dtappendix}_market_change", '.feather') + market_change_data.reset_index().to_feather( + filename_mc, compression_level=9, compression='lz4') + return filename @@ -57,12 +77,7 @@ def _store_backtest_analysis_data( :param dtappendix: Datetime to use for the filename :param name: Name to use for the file, e.g. signals, rejected """ - if recordfilename.is_dir(): - filename = (recordfilename / f'backtest-result-{dtappendix}_{name}.pkl') - else: - filename = Path.joinpath( - recordfilename.parent, f'{recordfilename.stem}-{dtappendix}_{name}.pkl' - ) + filename = _generate_filename(recordfilename, f"{dtappendix}_{name}", '.pkl') file_dump_joblib(filename, data) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 47aab2a627a..1bf73d71484 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -6,7 +6,7 @@ import numpy as np from pandas import DataFrame, Series, concat, to_datetime -from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, IntOrInf +from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, calculate_expectancy, calculate_market_change, calculate_max_drawdown, calculate_sharpe, calculate_sortino) @@ -71,7 +71,8 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column 'key': first_column, 'trades': len(result), 'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0, - 'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0, + 'profit_mean_pct': round(result['profit_ratio'].mean() * 100.0, 2 + ) if len(result) > 0 else 0.0, 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_total_abs': result['profit_abs'].sum(), @@ -154,42 +155,6 @@ def generate_tag_metrics(tag_type: str, return [] -def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> List[Dict]: - """ - Generate small table outlining Backtest results - :param max_open_trades: Max_open_trades parameter - :param results: Dataframe containing the backtest result for one strategy - :return: List of Dicts containing the metrics per Sell reason - """ - tabular_data = [] - - for reason, count in results['exit_reason'].value_counts().items(): - result = results.loc[results['exit_reason'] == reason] - - profit_mean = result['profit_ratio'].mean() - profit_sum = result['profit_ratio'].sum() - profit_total = profit_sum / max_open_trades - - tabular_data.append( - { - 'exit_reason': reason, - 'trades': count, - 'wins': len(result[result['profit_abs'] > 0]), - 'draws': len(result[result['profit_abs'] == 0]), - 'losses': len(result[result['profit_abs'] < 0]), - 'winrate': len(result[result['profit_abs'] > 0]) / count if count else 0.0, - 'profit_mean': profit_mean, - 'profit_mean_pct': round(profit_mean * 100, 2), - 'profit_sum': profit_sum, - 'profit_sum_pct': round(profit_sum * 100, 2), - 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': profit_total, - 'profit_total_pct': round(profit_total * 100, 2), - } - ) - return tabular_data - - def generate_strategy_comparison(bt_stats: Dict) -> List[Dict]: """ Generate summary per strategy @@ -383,9 +348,8 @@ def generate_strategy_stats(pairlist: List[str], enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance, results=results, skip_nan=False) - - exit_reason_stats = generate_exit_reason_stats(max_open_trades=max_open_trades, - results=results) + exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=start_balance, + results=results, skip_nan=False) left_open_results = generate_pair_metrics( pairlist, stake_currency=stake_currency, starting_balance=start_balance, results=results.loc[results['exit_reason'] == 'force_exit'], skip_nan=True) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index 81a9e7ad684..4d3bd5218b8 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -18,7 +18,7 @@ class _CustomData(ModelBase): """ CustomData database model Keeps records of metadata as key/value store - for trades or global persistant values + for trades or global persistent values One to many relationship with Trades: - One trade can have many metadata entries - One metadata entry can only be associated with one Trade diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index defeb0e3f5b..1421666ea05 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -847,7 +847,7 @@ def update_trade(self, order: Order, recalculating: bool = False) -> None: isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC) or (not recalculating and order.safe_amount_after_fee > amount_tr) ): - # When recalculating a trade, only comming out to 0 can force a close + # When recalculating a trade, only coming out to 0 can force a close self.close(order.safe_price) else: self.recalc_trade_from_orders() @@ -1125,7 +1125,7 @@ def recalc_trade_from_orders(self, *, is_closing: bool = False): prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price)) close_profit_abs += prof.profit_abs if total_stake > 0: - # This needs to be calculated based on the last occuring exit to be aligned + # This needs to be calculated based on the last occurring exit to be aligned # with realized_profit. close_profit = (close_profit_abs / total_stake) * self.leverage else: @@ -1538,7 +1538,7 @@ class Trade(ModelBase, LocalTrade): amount: Mapped[float] = mapped_column(Float()) # type: ignore amount_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore open_date: Mapped[datetime] = mapped_column( - nullable=False, default=datetime.utcnow) # type: ignore + nullable=False, default=datetime.now) # type: ignore close_date: Mapped[Optional[datetime]] = mapped_column() # type: ignore # absolute value of the stop loss stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e0aa2437a4a..4d29337a755 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -440,12 +440,12 @@ def create_scatter( def generate_candlestick_graph( pair: str, data: pd.DataFrame, trades: Optional[pd.DataFrame] = None, *, - indicators1: List[str] = [], indicators2: List[str] = [], - plot_config: Dict[str, Dict] = {}, + indicators1: Optional[List[str]] = None, indicators2: Optional[List[str]] = None, + plot_config: Optional[Dict[str, Dict]] = None, ) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB - Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators + Volume will always be plotted in row2, so Row 1 and 3 are to our disposal for custom indicators :param pair: Pair to Display on the graph :param data: OHLCV DataFrame containing indicators and entry/exit signals :param trades: All trades created @@ -454,7 +454,11 @@ def generate_candlestick_graph( :param plot_config: Dict of Dicts containing advanced plot configuration :return: Plotly figure """ - plot_config = create_plotconfig(indicators1, indicators2, plot_config) + plot_config = create_plotconfig( + indicators1 or [], + indicators2 or [], + plot_config or {}, + ) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] # Define the graph @@ -673,7 +677,7 @@ def plot_profit(config: Config) -> None: """ Plots the total profit for all pairs. Note, the profit calculation isn't realistic. - But should be somewhat proportional, and therefor useful + But should be somewhat proportional, and therefore useful in helping out to find a good algorithm. """ if 'timeframe' not in config: diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index a618f72d287..0c968f9880a 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -38,7 +38,7 @@ def __init__(self, exchange, pairlistmanager, self._refresh_period = self._pairlistconfig.get('refresh_period', 86400) self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) self._def_candletype = self._config['candle_type_def'] - self._coingekko: CoinGeckoAPI = CoinGeckoAPI() + self._coingecko: CoinGeckoAPI = CoinGeckoAPI() if self._max_rank > 250: raise OperationalException( @@ -127,7 +127,7 @@ def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: marketcap_list = self._marketcap_cache.get('marketcap') if marketcap_list is None: - data = self._coingekko.get_coins_markets(vs_currency='usd', order='market_cap_desc', + data = self._coingecko.get_coins_markets(vs_currency='usd', order='market_cap_desc', per_page='250', page='1', sparkline='false', locale='en') if data: diff --git a/freqtrade/plugins/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py index 4c878118457..f27fe035a13 100644 --- a/freqtrade/plugins/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -101,7 +101,7 @@ def available_parameters() -> Dict[str, PairlistParameter]: def _validate_pair(self, pair: str, ticker: Optional[Ticker]) -> bool: """ - Check if if one price-step (pip) is > than a certain barrier. + Check if one price-step (pip) is > than a certain barrier. :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.fetch_ticker :return: True if the pair can stay, false if it should be removed diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index 78e3c8351f5..0fe67968f70 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -116,7 +116,7 @@ def available_parameters() -> Dict[str, PairlistParameter]: "default": "filter", "options": ["filter", "append"], "description": "Processing mode", - "help": "Append pairs to incomming pairlist or filter them?", + "help": "Append pairs to incoming pairlist or filter them?", }, **IPairList.refresh_period_parameter(), "keep_pairlist_on_failure": { diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 25e0a855d3c..acc6ad7e117 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -65,7 +65,7 @@ def __init__(self, exchange, pairlistmanager, self._tf_in_min = timeframe_to_minutes(self._lookback_timeframe) _tf_in_sec = self._tf_in_min * 60 - # wether to use range lookback or not + # whether to use range lookback or not self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0) if self._use_range & (self._refresh_period < _tf_in_sec): diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 6057cbc296f..378eccfef7f 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -110,7 +110,7 @@ def calculate_lock_end(trades: List[LocalTrade], stop_minutes: int) -> datetime: Get lock end time """ max_date: datetime = max([trade.close_date for trade in trades if trade.close_date]) - # comming from Database, tzinfo is not set. + # coming from Database, tzinfo is not set. if max_date.tzinfo is None: max_date = max_date.replace(tzinfo=timezone.utc) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 1557f0f35b6..bcbb5704b39 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -47,7 +47,7 @@ class IResolver: @classmethod def build_search_paths(cls, config: Config, user_subdir: Optional[str] = None, - extra_dirs: List[str] = []) -> List[Path]: + extra_dirs: Optional[List[str]] = None) -> List[Path]: abs_paths: List[Path] = [] if cls.initial_search_path: @@ -57,8 +57,9 @@ def build_search_paths(cls, config: Config, user_subdir: Optional[str] = None, abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir)) # Add extra directory to the top of the search paths - for dir in extra_dirs: - abs_paths.insert(0, Path(dir).resolve()) + if extra_dirs: + for dir in extra_dirs: + abs_paths.insert(0, Path(dir).resolve()) if cls.extra_path and (extra := config.get(cls.extra_path)): abs_paths.insert(0, Path(extra).resolve()) @@ -139,7 +140,7 @@ def _search_object(cls, directory: Path, *, object_name: str, add_source: bool = @classmethod def _load_object(cls, paths: List[Path], *, object_name: str, add_source: bool = False, - kwargs: dict = {}) -> Optional[Any]: + kwargs: Dict) -> Optional[Any]: """ Try to load object from path list. """ @@ -163,7 +164,7 @@ def _load_object(cls, paths: List[Path], *, object_name: str, add_source: bool = def load_object(cls, object_name: str, config: Config, *, kwargs: dict, extra_dir: Optional[str] = None) -> Any: """ - Search and loads the specified object as configured in hte child class. + Search and loads the specified object as configured in the child class. :param object_name: name of the module to import :param config: configuration dictionary :param extra_dir: additional directory to search for the given pairlist diff --git a/freqtrade/rpc/api_server/api_auth.py b/freqtrade/rpc/api_server/api_auth.py index dd47491002a..257c1cc2402 100644 --- a/freqtrade/rpc/api_server/api_auth.py +++ b/freqtrade/rpc/api_server/api_auth.py @@ -26,6 +26,7 @@ def verify_auth(api_config, username: str, password: str): httpbasic = HTTPBasic(auto_error=False) +security = HTTPBasic() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) @@ -117,7 +118,7 @@ def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic) @router_login.post('/token/login', response_model=AccessAndRefreshToken) -def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()), +def token_login(form_data: HTTPBasicCredentials = Depends(security), api_config=Depends(get_api_config)): if verify_auth(api_config, form_data.username, form_data.password): diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 814dcf8da9c..345f835a470 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -10,15 +10,16 @@ from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.constants import Config -from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_result, - get_backtest_resultlist, load_and_merge_backtest_result, - update_backtest_metadata) +from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_market_change, + get_backtest_result, get_backtest_resultlist, + load_and_merge_backtest_result, update_backtest_metadata) from freqtrade.enums import BacktestState 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, - BacktestRequest, BacktestResponse) +from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMarketChange, + BacktestMetadataUpdate, BacktestRequest, + BacktestResponse) from freqtrade.rpc.api_server.deps import get_config from freqtrade.rpc.api_server.webserver_bgwork import ApiBG from freqtrade.rpc.rpc import RPCException @@ -32,8 +33,10 @@ def __run_backtest_bg(btconfig: Config): + from freqtrade.data.metrics import combined_dataframes_with_rel_mean from freqtrade.optimize.optimize_reports import generate_backtest_stats, store_backtest_stats from freqtrade.resolvers import StrategyResolver + asyncio.set_event_loop(asyncio.new_event_loop()) try: # Reload strategy @@ -89,11 +92,14 @@ def __run_backtest_bg(btconfig: Config): min_date=min_date, max_date=max_date) if btconfig.get('export', 'none') == 'trades': + combined_res = combined_dataframes_with_rel_mean(ApiBG.bt['data'], min_date, max_date) fn = store_backtest_stats( - btconfig['exportfilename'], ApiBG.bt['bt'].results, - datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + btconfig['exportfilename'], + ApiBG.bt['bt'].results, + datetime.now().strftime("%Y-%m-%d_%H-%M-%S"), + market_change_data=combined_res ) - ApiBG.bt['bt'].results['metadata'][strategy_name]['filename'] = str(fn.name) + ApiBG.bt['bt'].results['metadata'][strategy_name]['filename'] = str(fn.stem) ApiBG.bt['bt'].results['metadata'][strategy_name]['strategy'] = strategy_name logger.info("Backtest finished.") @@ -308,3 +314,20 @@ def api_update_backtest_history_entry(file: str, body: BacktestMetadataUpdate, raise HTTPException(status_code=400, detail=str(e)) return get_backtest_result(file_abs) + + +@router.get('/backtest/history/{file}/market_change', response_model=BacktestMarketChange, + tags=['webserver', 'backtest']) +def api_get_backtest_market_change(file: str, config=Depends(get_config)): + bt_results_base: Path = config['user_data_dir'] / 'backtest_results' + file_abs = (bt_results_base / f"{file}_market_change").with_suffix('.feather') + # Ensure file is in backtest_results directory + if not is_file_in_dir(file_abs, bt_results_base): + raise HTTPException(status_code=404, detail="File not found.") + df = get_backtest_market_change(file_abs) + + return { + 'columns': df.columns.tolist(), + 'data': df.values.tolist(), + 'length': len(df), + } diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index af8d8ddf46c..4e20a0f9da0 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,7 +1,7 @@ from datetime import date, datetime from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, RootModel, SerializeAsAny +from pydantic import AwareDatetime, BaseModel, RootModel, SerializeAsAny from freqtrade.constants import IntOrInf from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode @@ -378,6 +378,13 @@ class Locks(BaseModel): locks: List[LockModel] +class LocksPayload(BaseModel): + pair: str + side: str = '*' # Default to both sides + until: AwareDatetime + reason: Optional[str] = None + + class DeleteLockRequest(BaseModel): pair: Optional[str] = None lockid: Optional[int] = None @@ -482,12 +489,26 @@ class AvailablePairs(BaseModel): pair_interval: List[List[str]] +class PairCandlesRequest(BaseModel): + pair: str + timeframe: str + limit: Optional[int] = None + columns: Optional[List[str]] = None + + +class PairHistoryRequest(PairCandlesRequest): + timerange: str + strategy: str + freqaimodel: Optional[str] = None + + class PairHistory(BaseModel): strategy: str pair: str timeframe: str timeframe_ms: int columns: List[str] + all_columns: List[str] = [] data: SerializeAsAny[List[Any]] length: int buy_signals: int @@ -551,6 +572,12 @@ class BacktestMetadataUpdate(BaseModel): notes: str = '' +class BacktestMarketChange(BaseModel): + columns: List[str] + length: int + data: List[List[Any]] + + class SysInfo(BaseModel): cpu_pct: List[float] ram_pct: float diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 99fc3d4519b..2d3ba32fcd7 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -15,12 +15,13 @@ DeleteLockRequest, DeleteTrade, Entry, ExchangeListResponse, Exit, ForceEnterPayload, ForceEnterResponse, ForceExitPayload, - FreqAIModelListResponse, Health, Locks, Logs, - MixTag, OpenTradeSchema, PairHistory, - PerformanceEntry, Ping, PlotConfig, Profit, - ResultMsg, ShowConfig, Stats, StatusMsg, - StrategyListResponse, StrategyResponse, SysInfo, - Version, WhitelistResponse) + FreqAIModelListResponse, Health, Locks, + LocksPayload, Logs, MixTag, OpenTradeSchema, + PairCandlesRequest, PairHistory, + PairHistoryRequest, PerformanceEntry, Ping, + PlotConfig, Profit, ResultMsg, ShowConfig, Stats, + StatusMsg, StrategyListResponse, StrategyResponse, + SysInfo, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -53,7 +54,8 @@ # 2.32: new /backtest/history/ patch endpoint # 2.33: Additional weekly/monthly metrics # 2.34: new entries/exits/mix_tags endpoints -API_VERSION = 2.34 +# 2.35: pair_candles and pair_history endpoints as Post variant +API_VERSION = 2.35 # Public API, requires no auth. router_public = APIRouter() @@ -255,6 +257,13 @@ def delete_lock_pair(payload: DeleteLockRequest, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete_lock(lockid=payload.lockid, pair=payload.pair) +@router.post('/locks', response_model=Locks, tags=['info', 'locks']) +def add_locks(payload: List[LocksPayload], rpc: RPC = Depends(get_rpc)): + for lock in payload: + rpc._rpc_add_lock(lock.pair, lock.until, lock.reason, lock.side) + return rpc._rpc_locks() + + @router.get('/logs', response_model=Logs, tags=['info']) def logs(limit: Optional[int] = None): return RPC._rpc_get_logs(limit) @@ -284,7 +293,14 @@ def reload_config(rpc: RPC = Depends(get_rpc)): @router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) def pair_candles( pair: str, timeframe: str, limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_analysed_dataframe(pair, timeframe, limit) + return rpc._rpc_analysed_dataframe(pair, timeframe, limit, None) + + +@router.post('/pair_candles', response_model=PairHistory, tags=['candle data']) +def pair_candles_filtered(payload: PairCandlesRequest, rpc: RPC = Depends(get_rpc)): + # Advanced pair_candles endpoint with column filtering + return rpc._rpc_analysed_dataframe( + payload.pair, payload.timeframe, payload.limit, payload.columns) @router.get('/pair_history', response_model=PairHistory, tags=['candle data']) @@ -300,7 +316,25 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, 'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'), }) try: - return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange) + return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange, None) + except Exception as e: + raise HTTPException(status_code=502, detail=str(e)) + + +@router.post('/pair_history', response_model=PairHistory, tags=['candle data']) +def pair_history_filtered(payload: PairHistoryRequest, + config=Depends(get_config), exchange=Depends(get_exchange)): + # The initial call to this endpoint can be slow, as it may need to initialize + # the exchange class. + config = deepcopy(config) + config.update({ + 'strategy': payload.strategy, + 'timerange': payload.timerange, + 'freqaimodel': payload.freqaimodel if payload.freqaimodel else config.get('freqaimodel'), + }) + try: + return RPC._rpc_analysed_history_full( + config, payload.pair, payload.timeframe, exchange, payload.columns) except Exception as e: raise HTTPException(status_code=502, detail=str(e)) diff --git a/freqtrade/rpc/api_server/ws/channel.py b/freqtrade/rpc/api_server/ws/channel.py index 3c0a833d8bf..01bc7d276df 100644 --- a/freqtrade/rpc/api_server/ws/channel.py +++ b/freqtrade/rpc/api_server/ws/channel.py @@ -152,7 +152,7 @@ def is_closed(self) -> bool: """ return self._closed.is_set() - def set_subscriptions(self, subscriptions: List[str] = []) -> None: + def set_subscriptions(self, subscriptions: List[str]) -> None: """ Set which subscriptions this channel is subscribed to diff --git a/freqtrade/rpc/external_message_consumer.py b/freqtrade/rpc/external_message_consumer.py index 200d408e6da..bb0b3139f11 100644 --- a/freqtrade/rpc/external_message_consumer.py +++ b/freqtrade/rpc/external_message_consumer.py @@ -237,7 +237,7 @@ async def _create_connection(self, producer: Producer, lock: asyncio.Lock): continue except Exception as e: - # An unforseen error has occurred, log and continue + # An unforeseen error has occurred, log and continue logger.error("Unexpected error has occurred:") logger.exception(e) await asyncio.sleep(self.sleep_time) @@ -387,7 +387,7 @@ def _consume_analyzed_df_message(self, producer_name: str, message: WSMessageSch ) if not did_append: - # We want an overlap in candles incase some data has changed + # We want an overlap in candles in case some data has changed n_missing += 1 # Set to None for all candles if we missed a full df's worth of candles n_missing = n_missing if n_missing < FULL_DATAFRAME_THRESHOLD else 1500 diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index d01596cfc3c..2b44d054606 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -39,7 +39,7 @@ class CryptoToFiatConverter(LoggingMixin): This object is also a Singleton """ __instance = None - _coingekko: CoinGeckoAPI = None + _coingecko: CoinGeckoAPI = None _coinlistings: List[Dict] = [] _backoff: float = 0.0 @@ -52,9 +52,9 @@ def __new__(cls): try: # Limit retires to 1 (0 and 1) # otherwise we risk bot impact if coingecko is down. - CryptoToFiatConverter._coingekko = CoinGeckoAPI(retries=1) + CryptoToFiatConverter._coingecko = CoinGeckoAPI(retries=1) except BaseException: - CryptoToFiatConverter._coingekko = None + CryptoToFiatConverter._coingecko = None return CryptoToFiatConverter.__instance def __init__(self) -> None: @@ -67,7 +67,7 @@ def __init__(self) -> None: def _load_cryptomap(self) -> None: try: # Use list-comprehension to ensure we get a list. - self._coinlistings = [x for x in self._coingekko.get_coins_list()] + self._coinlistings = [x for x in self._coingecko.get_coins_list()] except RequestException as request_exception: if "429" in str(request_exception): logger.warning( @@ -84,7 +84,7 @@ def _load_cryptomap(self) -> None: logger.error( f"Could not load FIAT Cryptocurrency map for the following problem: {exception}") - def _get_gekko_id(self, crypto_symbol): + def _get_gecko_id(self, crypto_symbol): if not self._coinlistings: if self._backoff <= datetime.now().timestamp(): self._load_cryptomap() @@ -180,9 +180,9 @@ def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float: if crypto_symbol == fiat_symbol: return 1.0 - _gekko_id = self._get_gekko_id(crypto_symbol) + _gecko_id = self._get_gecko_id(crypto_symbol) - if not _gekko_id: + if not _gecko_id: # return 0 for unsupported stake currencies (fiat-convert should not break the bot) self.log_once( f"unsupported crypto-symbol {crypto_symbol.upper()} - returning 0.0", @@ -191,10 +191,10 @@ def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float: try: return float( - self._coingekko.get_price( - ids=_gekko_id, + self._coingecko.get_price( + ids=_gecko_id, vs_currencies=fiat_symbol - )[_gekko_id][fiat_symbol] + )[_gecko_id][fiat_symbol] ) except Exception as exception: logger.error("Error in _find_price: %s", exception) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8bb7f754fb2..87f5688c701 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -16,7 +16,7 @@ from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange -from freqtrade.constants import CANCEL_REASON, Config +from freqtrade.constants import CANCEL_REASON, DEFAULT_DATAFRAME_COLUMNS, Config from freqtrade.data.history import load_data from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection, @@ -30,8 +30,8 @@ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.rpc_types import RPCSendMsg -from freqtrade.util import (decimals_per_coin, dt_humanize, dt_now, dt_ts_def, format_date, - shorten_date) +from freqtrade.util import decimals_per_coin, dt_now, dt_ts_def, format_date, shorten_date +from freqtrade.util.datetime_helpers import dt_humanize_delta from freqtrade.wallets import PositionWallet, Wallet @@ -155,7 +155,7 @@ def _rpc_show_config(config, botstate: Union[State, str], } return val - def _rpc_trade_status(self, trade_ids: List[int] = []) -> List[Dict[str, Any]]: + def _rpc_trade_status(self, trade_ids: Optional[List[int]] = None) -> List[Dict[str, Any]]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function @@ -301,13 +301,13 @@ def _rpc_status_table(self, stake_currency: str, for oo in trade.open_orders ] - # exemple: '*.**.**' trying to enter, exit and exit with 3 different orders + # example: '*.**.**' trying to enter, exit and exit with 3 different orders active_attempt_side_symbols_str = '.'.join(active_attempt_side_symbols) detail_trade = [ f'{trade.id} {direction_str}', trade.pair + active_attempt_side_symbols_str, - shorten_date(dt_humanize(trade.open_date, only_distance=True)), + shorten_date(dt_humanize_delta(trade.open_date_utc)), profit_str ] @@ -460,8 +460,11 @@ def trade_win_loss(trade): def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str, - start_date: datetime = datetime.fromtimestamp(0)) -> Dict[str, Any]: + start_date: Optional[datetime] = None) -> Dict[str, Any]: """ Returns cumulative profit statistics """ + + start_date = datetime.fromtimestamp(0) if start_date is None else start_date + trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) | Trade.is_open.is_(True)) trades: Sequence[Trade] = Trade.session.scalars(Trade.get_trades_query( @@ -596,10 +599,10 @@ def _rpc_trade_statistics( 'trade_count': len(trades), 'closed_trade_count': closed_trade_count, 'first_trade_date': format_date(first_date), - 'first_trade_humanized': dt_humanize(first_date) if first_date else '', + 'first_trade_humanized': dt_humanize_delta(first_date) if first_date else '', 'first_trade_timestamp': dt_ts_def(first_date, 0), 'latest_trade_date': format_date(last_date), - 'latest_trade_humanized': dt_humanize(last_date) if last_date else '', + 'latest_trade_humanized': dt_humanize_delta(last_date) if last_date else '', 'latest_trade_timestamp': dt_ts_def(last_date, 0), 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_pair': best_pair[0] if best_pair else '', @@ -1104,6 +1107,16 @@ def _rpc_delete_lock(self, lockid: Optional[int] = None, return self._rpc_locks() + def _rpc_add_lock( + self, pair: str, until: datetime, reason: Optional[str], side: str) -> PairLock: + lock = PairLocks.lock_pair( + pair=pair, + until=until, + reason=reason, + side=side, + ) + return lock + def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name_list, @@ -1177,9 +1190,11 @@ def _rpc_edge(self) -> List[Dict[str, Any]]: return self._freqtrade.edge.accepted_pairs() @staticmethod - def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame, - last_analyzed: datetime) -> Dict[str, Any]: + def _convert_dataframe_to_dict( + strategy: str, pair: str, timeframe: str, dataframe: DataFrame, + last_analyzed: datetime, selected_cols: Optional[List[str]]) -> Dict[str, Any]: has_content = len(dataframe) != 0 + dataframe_columns = list(dataframe.columns) signals = { 'enter_long': 0, 'exit_long': 0, @@ -1187,6 +1202,11 @@ def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, datafra 'exit_short': 0, } if has_content: + if selected_cols is not None: + # Ensure OHLCV columns are always present + cols_set = set(DEFAULT_DATAFRAME_COLUMNS + list(signals.keys()) + selected_cols) + df_cols = [col for col in dataframe_columns if col in cols_set] + dataframe = dataframe.loc[:, df_cols] dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 # Move signal close to separate column when signal for easy plotting @@ -1211,6 +1231,7 @@ def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, datafra 'timeframe': timeframe, 'timeframe_ms': timeframe_to_msecs(timeframe), 'strategy': strategy, + 'all_columns': dataframe_columns, 'columns': list(dataframe.columns), 'data': dataframe.values.tolist(), 'length': len(dataframe), @@ -1236,13 +1257,16 @@ def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, datafra }) return res - def _rpc_analysed_dataframe(self, pair: str, timeframe: str, - limit: Optional[int]) -> Dict[str, Any]: + def _rpc_analysed_dataframe( + self, pair: str, timeframe: str, limit: Optional[int], + selected_cols: Optional[List[str]]) -> Dict[str, Any]: """ Analyzed dataframe in Dict form """ _data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit) - return RPC._convert_dataframe_to_dict(self._freqtrade.config['strategy'], - pair, timeframe, _data, last_analyzed) + return RPC._convert_dataframe_to_dict( + self._freqtrade.config['strategy'], pair, timeframe, _data, last_analyzed, + selected_cols + ) def __rpc_analysed_dataframe_raw( self, @@ -1309,7 +1333,7 @@ def _ws_request_whitelist(self): @staticmethod def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str, - exchange) -> Dict[str, Any]: + exchange, selected_cols: Optional[List[str]]) -> Dict[str, Any]: timerange_parsed = TimeRange.parse_timerange(config.get('timerange')) from freqtrade.data.converter import trim_dataframe @@ -1339,7 +1363,8 @@ def _rpc_analysed_history_full(config: Config, pair: str, timeframe: str, df_analyzed = trim_dataframe(df_analyzed, timerange_parsed, startup_candles=startup_candles) return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, - df_analyzed.copy(), dt_now()) + df_analyzed.copy(), dt_now(), + selected_cols) def _rpc_plot_config(self) -> Dict[str, Any]: if (self._freqtrade.strategy.plot_config and diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b1da5954953..030075c648e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -33,7 +33,7 @@ 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, format_date, round_value +from freqtrade.util import dt_from_ts, dt_humanize_delta, fmt_coin, format_date, round_value MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH @@ -488,7 +488,7 @@ def compose_message(self, msg: RPCSendMsg) -> Optional[str]: elif msg['type'] == RPCMessageType.WARNING: message = f"\N{WARNING SIGN} *Warning:* `{msg['status']}`" elif msg['type'] == RPCMessageType.EXCEPTION: - # Errors will contain exceptions, which are wrapped in tripple ticks. + # Errors will contain exceptions, which are wrapped in triple ticks. message = f"\N{WARNING SIGN} *ERROR:* \n {msg['status']}" elif msg['type'] == RPCMessageType.STARTUP: @@ -573,8 +573,7 @@ def _prepare_order_details(self, filled_orders: List, quote_currency: str, is_op # 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("({})".format(dt_humanize_delta(order["order_filled_date"]))) lines.append(f"*Amount:* {round_value(cur_entry_amount, 8)} " f"({fmt_coin(order['cost'], quote_currency)})") lines.append(f"*Average {wording} Price:* {round_value(cur_entry_average, 8)} " @@ -657,7 +656,7 @@ async def _status_msg(self, update: Update, context: CallbackContext) -> None: position_adjust = self._config.get('position_adjustment_enable', False) max_entries = self._config.get('max_entry_position_adjustment', -1) for r in results: - r['open_date_hum'] = dt_humanize(r['open_date']) + r['open_date_hum'] = dt_humanize_delta(r['open_date']) r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']]) r['num_exits'] = len([o for o in r['orders'] if not o['ft_is_entry'] and not o['ft_order_side'] == 'stoploss']) @@ -1174,7 +1173,7 @@ async def _force_exit(self, update: Update, context: CallbackContext) -> None: text='Cancel', callback_data='force_exit__cancel')]) await self._send_msg(msg="Which trade?", keyboard=buttons_aligned) - async def _force_exit_action(self, trade_id): + async def _force_exit_action(self, trade_id: str): if trade_id != 'cancel': try: loop = asyncio.get_running_loop() @@ -1289,7 +1288,7 @@ async def _trades(self, update: Update, context: CallbackContext) -> None: nrecent ) trades_tab = tabulate( - [[dt_humanize(trade['close_date']), + [[dt_humanize_delta(dt_from_ts(trade['close_timestamp'])), trade['pair'] + " (#" + str(trade['trade_id']) + ")", f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})"] for trade in trades['trades']], @@ -1549,7 +1548,7 @@ async def _blacklist(self, update: Update, context: CallbackContext) -> None: async def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] - for pair, error in blacklist['errors'].items(): + for _, error in blacklist['errors'].items(): errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: await self._send_msg('\n'.join(errmsgs)) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index e83d9433d41..6e44a7e2024 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -64,7 +64,7 @@ def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFr def decorator(fn: PopulateIndicators): informative_pairs = getattr(fn, '_ft_informative', []) informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill, _candle_type)) - setattr(fn, '_ft_informative', informative_pairs) + setattr(fn, '_ft_informative', informative_pairs) # noqa: B010 return fn return decorator diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index b0fc538ca4e..5085063a353 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -78,7 +78,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, # all indicators on the informative sample MUST be calculated before this point if ffill: # https://pandas.pydata.org/docs/user_guide/merging.html#timeseries-friendly-merging - # merge_ordered - ffill method is 2.5x faster than seperate ffill() + # merge_ordered - ffill method is 2.5x faster than separate ffill() dataframe = pd.merge_ordered(dataframe, informative, fill_method="ffill", left_on='date', right_on=date_merge, how='left') else: diff --git a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 index 541c26e8736..1783e818c80 100644 --- a/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/strategy_subtemplates/strategy_methods_advanced.j2 @@ -3,7 +3,7 @@ def bot_loop_start(self, current_time: datetime, **kwargs) -> None: """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks - (e.g. gather some remote ressource for comparison) + (e.g. gather some remote resource for comparison) For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index f7e63d9d3d7..6f523cd8ebd 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -1,8 +1,9 @@ -from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, - dt_ts_def, dt_ts_none, dt_utc, format_date, +from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize_delta, dt_now, + dt_ts, dt_ts_def, dt_ts_none, dt_utc, format_date, format_ms_time, shorten_date) from freqtrade.util.formatters import decimals_per_coin, fmt_coin, round_value from freqtrade.util.ft_precise import FtPrecise +from freqtrade.util.measure_time import MeasureTime from freqtrade.util.periodic_cache import PeriodicCache from freqtrade.util.template_renderer import render_template, render_template_with_fallback # noqa @@ -10,7 +11,7 @@ __all__ = [ 'dt_floor_day', 'dt_from_ts', - 'dt_humanize', + 'dt_humanize_delta', 'dt_now', 'dt_ts', 'dt_ts_def', @@ -24,4 +25,5 @@ 'decimals_per_coin', 'round_value', 'fmt_coin', + 'MeasureTime', ] diff --git a/freqtrade/util/datetime_helpers.py b/freqtrade/util/datetime_helpers.py index 66b738e8d63..53878ca84af 100644 --- a/freqtrade/util/datetime_helpers.py +++ b/freqtrade/util/datetime_helpers.py @@ -1,8 +1,9 @@ import re from datetime import datetime, timezone -from typing import Optional +from time import time +from typing import Optional, Union -import arrow +import humanize from freqtrade.constants import DATETIME_PRINT_FORMAT @@ -25,7 +26,7 @@ def dt_ts(dt: Optional[datetime] = None) -> int: """ if dt: return int(dt.timestamp() * 1000) - return int(dt_now().timestamp() * 1000) + return int(time() * 1000) def dt_ts_def(dt: Optional[datetime], default: int = 0) -> int: @@ -76,13 +77,11 @@ def shorten_date(_date: str) -> str: return new_date -def dt_humanize(dt: datetime, **kwargs) -> str: +def dt_humanize_delta(dt: datetime): """ - Return a humanized string for the given datetime. - :param dt: datetime to humanize - :param kwargs: kwargs to pass to arrow's humanize() + Return a humanized string for the given timedelta. """ - return arrow.get(dt).humanize(**kwargs) + return humanize.naturaltime(dt) def format_date(date: Optional[datetime]) -> str: @@ -96,9 +95,9 @@ def format_date(date: Optional[datetime]) -> str: return '' -def format_ms_time(date: int) -> str: +def format_ms_time(date: Union[int, float]) -> str: """ convert MS date to readable format. : epoch-string in ms """ - return datetime.fromtimestamp(date / 1000.0).strftime('%Y-%m-%dT%H:%M:%S') + return dt_from_ts(date).strftime('%Y-%m-%dT%H:%M:%S') diff --git a/freqtrade/util/measure_time.py b/freqtrade/util/measure_time.py new file mode 100644 index 00000000000..8266adfc05c --- /dev/null +++ b/freqtrade/util/measure_time.py @@ -0,0 +1,43 @@ +import logging +import time +from typing import Callable + +from cachetools import TTLCache + + +logger = logging.getLogger(__name__) + + +class MeasureTime: + """ + Measure the time of a block of code and call a callback if the time limit is exceeded. + """ + def __init__( + self, callback: Callable[[float, float], None], time_limit: float, ttl: int = 3600 * 4): + """ + :param callback: The callback to call if the time limit is exceeded. + This callback will be called once every "ttl" seconds, + with the parameters "duration" (in seconds) and + "time limit" - representing the passed in time limit. + :param time_limit: The time limit in seconds. + :param ttl: The time to live of the cache in seconds. + defaults to 4 hours. + """ + self._callback = callback + self._time_limit = time_limit + self.__cache: TTLCache = TTLCache(maxsize=1, ttl=ttl) + + def __enter__(self): + self._start = time.time() + + def __exit__(self, *args): + end = time.time() + if self.__cache.get('value'): + return + duration = end - self._start + + if duration < self._time_limit: + return + self._callback(duration, self._time_limit) + + self.__cache['value'] = True diff --git a/freqtrade/util/template_renderer.py b/freqtrade/util/template_renderer.py index 362d0f8757d..a875818bf89 100644 --- a/freqtrade/util/template_renderer.py +++ b/freqtrade/util/template_renderer.py @@ -3,7 +3,10 @@ """ -def render_template(templatefile: str, arguments: dict = {}) -> str: +from typing import Dict, Optional + + +def render_template(templatefile: str, arguments: Dict) -> str: from jinja2 import Environment, PackageLoader, select_autoescape @@ -16,11 +19,13 @@ def render_template(templatefile: str, arguments: dict = {}) -> str: def render_template_with_fallback(templatefile: str, templatefallbackfile: str, - arguments: dict = {}) -> str: + arguments: Optional[Dict] = None) -> str: """ Use templatefile if possible, otherwise fall back to templatefallbackfile """ from jinja2.exceptions import TemplateNotFound + if arguments is None: + arguments = {} try: return render_template(templatefile, arguments) except TemplateNotFound: diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 0d22feb36b3..a1038b368c3 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -70,7 +70,7 @@ def get_total(self, currency: str) -> float: def _update_dry(self) -> None: """ Update from database in dry-run mode - - Apply apply profits of closed trades on top of stake amount + - Apply profits of closed trades on top of stake amount - Subtract currently tied up stake_amount in open trades - update balances for currencies currently in trades """ @@ -306,7 +306,7 @@ def get_trade_stake_amount( :raise: DependencyException if the available stake amount is too low """ stake_amount: float - # Ensure wallets are uptodate. + # Ensure wallets are up-to-date. if update: self.update() val_tied_up = Trade.total_open_trades_stakes() diff --git a/freqtrade/worker.py b/freqtrade/worker.py index fb89e7a2d90..e9dbfa74b51 100644 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -137,7 +137,7 @@ def _throttle(self, func: Callable[..., Any], throttle_secs: float, Throttles the given callable that it takes at least `min_secs` to finish execution. :param func: Any callable - :param throttle_secs: throttling interation execution time limit in seconds + :param throttle_secs: throttling iteration execution time limit in seconds :param timeframe: ensure iteration is executed at the beginning of the next candle. :param timeframe_offset: offset in seconds to apply to the next candle time. :return: Any (result of execution of func) diff --git a/ft_client/freqtrade_client/__init__.py b/ft_client/freqtrade_client/__init__.py index 67cd641c436..814617f0031 100644 --- a/ft_client/freqtrade_client/__init__.py +++ b/ft_client/freqtrade_client/__init__.py @@ -1,7 +1,7 @@ from freqtrade_client.ft_rest_client import FtRestClient -__version__ = '2024.3' +__version__ = '2024.4' if 'dev' in __version__: from pathlib import Path diff --git a/ft_client/freqtrade_client/ft_client.py b/ft_client/freqtrade_client/ft_client.py index 96a7510fff8..f76780ed015 100644 --- a/ft_client/freqtrade_client/ft_client.py +++ b/ft_client/freqtrade_client/ft_client.py @@ -20,7 +20,10 @@ def add_arguments(args: Any = None): - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser( + prog="freqtrade-client", + description="Client for the freqtrade REST API", + ) parser.add_argument("command", help="Positional argument defining the command to execute.", nargs="?" @@ -67,7 +70,7 @@ 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): + for x, _ 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") diff --git a/ft_client/freqtrade_client/ft_rest_client.py b/ft_client/freqtrade_client/ft_rest_client.py index de782ee6584..56290a9d83f 100755 --- a/ft_client/freqtrade_client/ft_rest_client.py +++ b/ft_client/freqtrade_client/ft_rest_client.py @@ -7,7 +7,7 @@ import json import logging -from typing import Optional +from typing import Any, Dict, List, Optional, Union from urllib.parse import urlencode, urlparse, urlunparse import requests @@ -16,6 +16,9 @@ logger = logging.getLogger("ft_rest_client") +ParamsT = Optional[Dict[str, Any]] +PostDataT = Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] + class FtRestClient: @@ -58,13 +61,13 @@ def _call(self, method, apipath, params: Optional[dict] = None, data=None, files except ConnectionError: logger.warning("Connection error") - def _get(self, apipath, params: Optional[dict] = None): + def _get(self, apipath, params: ParamsT = None): return self._call("GET", apipath, params=params) - def _delete(self, apipath, params: Optional[dict] = None): + def _delete(self, apipath, params: ParamsT = None): return self._call("DELETE", apipath, params=params) - def _post(self, apipath, params: Optional[dict] = None, data: Optional[dict] = None): + def _post(self, apipath, params: ParamsT = None, data: PostDataT = None): return self._call("POST", apipath, params=params, data=data) def start(self): @@ -148,6 +151,25 @@ def delete_lock(self, lock_id): """ return self._delete(f"locks/{lock_id}") + def lock_add(self, pair: str, until: str, side: str = '*', reason: str = ''): + """Lock pair + + :param pair: Pair to lock + :param until: Lock until this date (format "2024-03-30 16:00:00Z") + :param side: Side to lock (long, short, *) + :param reason: Reason for the lock + :return: json object + """ + data = [ + { + "pair": pair, + "until": until, + "side": side, + "reason": reason + } + ] + return self._post("locks", data=data) + def daily(self, days=None): """Return the profits for each day, and amount of trades. @@ -375,12 +397,13 @@ def available_pairs(self, timeframe=None, stake_currency=None): "timeframe": timeframe if timeframe else '', }) - def pair_candles(self, pair, timeframe, limit=None): + def pair_candles(self, pair, timeframe, limit=None, columns=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. + :param columns: List of dataframe columns to return. Empty list will return OHLCV. :return: json object """ params = { @@ -389,6 +412,14 @@ def pair_candles(self, pair, timeframe, limit=None): } if limit: params['limit'] = limit + + if columns is not None: + params['columns'] = columns + return self._post( + "pair_candles", + data=params + ) + return self._get("pair_candles", params=params) def pair_history(self, pair, timeframe, strategy, timerange=None, freqaimodel=None): diff --git a/ft_client/test_client/test_rest_client.py b/ft_client/test_client/test_rest_client.py index e7c2f32e693..13e32f1c5cb 100644 --- a/ft_client/test_client/test_rest_client.py +++ b/ft_client/test_client/test_rest_client.py @@ -61,6 +61,7 @@ def test_FtRestClient_call_invalid(caplog): ('exits', []), ('mix_tags', []), ('locks', []), + ('lock_add', ["XRP/USDT", '2024-01-01 20:00:00Z', '*', 'rand']), ('delete_lock', [2]), ('daily', []), ('daily', [15]), diff --git a/pyproject.toml b/pyproject.toml index f7a244c8bdf..5e02079a2e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,8 @@ ignore_missing_imports = true namespace_packages = false warn_unused_ignores = true exclude = [ - '^build_helpers\.py$' + '^build_helpers\.py$', + '^ft_client/build/.*$', ] plugins = [ "sqlalchemy.ext.mypy.plugin" @@ -121,6 +122,7 @@ target-version = "py38" # Exclude UP036 as it's causing the "exit if < 3.9" to fail. extend-select = [ "C90", # mccabe + # "B", # bugbear # "N", # pep8-naming "F", # pyflakes "E", # pycodestyle @@ -128,6 +130,7 @@ extend-select = [ "UP", # pyupgrade "TID", # flake8-tidy-imports # "EXE", # flake8-executable + # "C4", # flake8-comprehensions "YTT", # flake8-2020 # "S", # flake8-bandit # "DTZ", # flake8-datetimez @@ -140,6 +143,7 @@ extend-ignore = [ "E241", # Multiple spaces after comma "E272", # Multiple spaces before keyword "E221", # Multiple spaces before operator + "B007", # Loop control variable not used ] [tool.ruff.lint.mccabe] @@ -148,6 +152,10 @@ max-complexity = 12 [tool.ruff.lint.per-file-ignores] "tests/*" = ["S"] +[tool.ruff.lint.flake8-bugbear] +# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`. +extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] + [tool.flake8] # Default from https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-ignore # minus E226 @@ -162,3 +170,7 @@ exclude = [ ".venv", ".env", ] + +[tool.codespell] +ignore-words-list = "coo,fo,strat,zar,selectin" +skip="*.svg,./user_data,./freqtrade/rpc/api_server/ui/installed" diff --git a/requirements-dev.txt b/requirements-dev.txt index 9fbb7b667e3..9d0ca0dcfe3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,15 +7,15 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.3.4 -mypy==1.9.0 +ruff==0.4.2 +mypy==1.10.0 pre-commit==3.7.0 -pytest==8.1.1 +pytest==8.2.0 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 +pytest-xdist==3.6.1 isort==5.13.2 # For datetime mocking time-machine==2.14.1 @@ -26,6 +26,6 @@ nbconvert==7.16.3 # mypy types types-cachetools==5.3.0.7 types-filelock==3.2.7 -types-requests==2.31.0.20240311 +types-requests==2.31.0.20240406 types-tabulate==0.9.0.20240106 types-python-dateutil==2.9.0.20240316 diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 3f623b0c0c5..287da4862cb 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -2,9 +2,9 @@ -r requirements-freqai.txt # Required for freqai-rl -torch==2.2.1 +torch==2.2.2 gymnasium==0.29.1 -stable_baselines3==2.2.1 +stable_baselines3==2.3.2 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 31366efa795..6bab60ddcd0 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -3,9 +3,9 @@ -r requirements-plot.txt # Required for freqai -scikit-learn==1.4.1.post1 -joblib==1.3.2 -catboost==1.2.3; 'arm' not in platform_machine +scikit-learn==1.4.2 +joblib==1.4.0 +catboost==1.2.5; 'arm' not in platform_machine lightgbm==4.3.0 xgboost==2.0.3 tensorboard==2.16.2 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 5347adf9c34..7ab39a42d2e 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.12.0 -scikit-learn==1.4.1.post1 +scipy==1.13.0 +scikit-learn==1.4.2 ft-scikit-optimize==0.9.2 -filelock==3.13.1 +filelock==3.13.4 diff --git a/requirements-plot.txt b/requirements-plot.txt index 0d3c404f501..2ef73837270 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.20.0 +plotly==5.21.0 diff --git a/requirements.txt b/requirements.txt index 9f6505618e5..40776554581 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ numpy==1.26.4 -pandas==2.2.1 +pandas==2.2.2 pandas-ta==0.3.14b -ccxt==4.2.82 +ccxt==4.3.11 cryptography==42.0.5 -aiohttp==3.9.3 +aiohttp==3.9.5 SQLAlchemy==2.0.29 -python-telegram-bot==21.0.1 +python-telegram-bot==21.1.1 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 -arrow==1.3.0 +humanize==4.9.0 cachetools==5.3.3 requests==2.31.0 urllib3==2.2.1 @@ -20,9 +20,9 @@ tabulate==0.9.0 pycoingecko==3.1.0 jinja2==3.1.3 tables==3.9.1 -joblib==1.3.2 +joblib==1.4.0 rich==13.7.1 -pyarrow==15.0.2; platform_machine != 'armv7l' +pyarrow==16.0.0; platform_machine != 'armv7l' # find first, C search in arrays py_find_1st==1.1.6 @@ -30,14 +30,14 @@ py_find_1st==1.1.6 # Load ticker files 30% faster python-rapidjson==1.16 # Properly format api responses -orjson==3.9.15 +orjson==3.10.1 # Notify systemd sdnotify==0.3.2 # API Server -fastapi==0.110.0 -pydantic==2.6.4 +fastapi==0.110.2 +pydantic==2.7.1 uvicorn==0.29.0 pyjwt==2.8.0 aiofiles==23.2.1 @@ -59,5 +59,5 @@ schedule==1.2.1 websockets==12.0 janus==1.0.0 -ast-comments==1.2.1 +ast-comments==1.2.2 packaging==24.0 diff --git a/scripts/ws_client.py b/scripts/ws_client.py index 5d27f512e68..818426da2fd 100755 --- a/scripts/ws_client.py +++ b/scripts/ws_client.py @@ -191,7 +191,7 @@ async def _handle_analyzed_df(self, name, type, data): self.logger.info("Empty DataFrame") async def _handle_default(self, name, type, data): - self.logger.info("Unkown message of type {type} received...") + self.logger.info("Unknown message of type {type} received...") self.logger.info(data) @@ -201,7 +201,7 @@ async def create_client( token, scheme='ws', name='default', - protocol=ClientProtocol(), + protocol=None, sleep_time=10, ping_timeout=10, wait_timeout=30, @@ -216,6 +216,8 @@ async def create_client( :param name: The name of the producer :param **kwargs: Any extra kwargs passed to websockets.connect """ + if not protocol: + protocol = ClientProtocol() while 1: try: @@ -277,7 +279,7 @@ async def create_client( continue except Exception as e: - # An unforseen error has occurred, log and try reconnecting again + # An unforeseen error has occurred, log and try reconnecting again logger.error("Unexpected error has occurred:") logger.exception(e) diff --git a/setup.py b/setup.py index f292a534949..504d3b2b74d 100644 --- a/setup.py +++ b/setup.py @@ -73,14 +73,14 @@ 'ccxt>=4.2.47', 'SQLAlchemy>=2.0.6', 'python-telegram-bot>=20.1', - 'arrow>=1.0.0', + 'humanize>=4.0.0', 'cachetools', 'requests', 'httpx>=0.24.1', 'urllib3', 'jsonschema', 'numpy', - 'pandas', + 'pandas>=2.2.0,<3.0', 'TA-Lib', 'pandas-ta', 'technical', diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 789535a46c3..630950c8171 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1609,4 +1609,4 @@ def test_start_show_config(capsys, caplog): 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) + assert log_has_re(r'Sensitive information will be shown in the upcoming output.*', caplog) diff --git a/tests/conftest.py b/tests/conftest.py index d894a790873..b46f30f8f83 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,10 +49,10 @@ def pytest_addoption(parser): def pytest_configure(config): config.addinivalue_line( - "markers", "longrun: mark test that is running slowly and should not be run regularily" + "markers", "longrun: mark test that is running slowly and should not be run regularly" ) if not config.option.longrun: - setattr(config.option, 'markexpr', 'not longrun') + config.option.markexpr = 'not longrun' class FixtureScheduler(LoadScopeScheduling): @@ -490,10 +490,10 @@ def user_dir(mocker, tmp_path) -> Path: @pytest.fixture(autouse=True) -def patch_coingekko(mocker) -> None: +def patch_coingecko(mocker) -> None: """ - Mocker to coingekko to speed up tests - :param mocker: mocker to patch coingekko class + Mocker to coingecko to speed up tests + :param mocker: mocker to patch coingecko class :return: None """ diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 554ee261a11..7d7a973316e 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -16,7 +16,7 @@ calculate_expectancy, calculate_market_change, calculate_max_drawdown, calculate_sharpe, calculate_sortino, calculate_underwater, combine_dataframes_with_mean, - create_cum_profit) + combined_dataframes_with_rel_mean, create_cum_profit) from freqtrade.exceptions import OperationalException from freqtrade.util import dt_utc from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades @@ -251,10 +251,29 @@ def test_combine_dataframes_with_mean(testdatadir): assert "mean" in df.columns +def test_combined_dataframes_with_rel_mean(testdatadir): + pairs = ["ETH/BTC", "ADA/BTC"] + data = load_data(datadir=testdatadir, pairs=pairs, timeframe='5m') + df = combined_dataframes_with_rel_mean( + data, + datetime(2018, 1, 12, tzinfo=timezone.utc), + datetime(2018, 1, 28, tzinfo=timezone.utc) + ) + assert isinstance(df, DataFrame) + assert "ETH/BTC" not in df.columns + assert "ADA/BTC" not in df.columns + assert "mean" in df.columns + assert "rel_mean" in df.columns + assert "count" in df.columns + assert df.iloc[0]['count'] == 2 + assert df.iloc[-1]['count'] == 2 + assert len(df) < len(data['ETH/BTC']) + + def test_combine_dataframes_with_mean_no_data(testdatadir): pairs = ["ETH/BTC", "ADA/BTC"] data = load_data(datadir=testdatadir, pairs=pairs, timeframe='6m') - with pytest.raises(ValueError, match=r"No objects to concatenate"): + with pytest.raises(ValueError, match=r"No data provided\."): combine_dataframes_with_mean(data) @@ -463,12 +482,12 @@ def test_calculate_max_drawdown2(): assert drawdown == 0.043965 -@pytest.mark.parametrize('profits,relative,highd,lowd,result,result_rel', [ +@pytest.mark.parametrize('profits,relative,highd,lowdays,result,result_rel', [ ([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909), ([0.0, -500.0, 500.0, 10000.0, -1000.0], True, 0, 1, 500.0, 0.5), ]) -def test_calculate_max_drawdown_abs(profits, relative, highd, lowd, result, result_rel): +def test_calculate_max_drawdown_abs(profits, relative, highd, lowdays, result, result_rel): """ Test case from issue https://github.com/freqtrade/freqtrade/issues/6655 [1000, 500, 1000, 11000, 10000] # absolute results @@ -488,7 +507,7 @@ def test_calculate_max_drawdown_abs(profits, relative, highd, lowd, result, resu assert isinstance(drawdown, float) assert isinstance(drawdown_rel, float) assert hdate == init_date + timedelta(days=highd) - assert ldate == init_date + timedelta(days=lowd) + assert ldate == init_date + timedelta(days=lowdays) # High must be before low assert hdate < ldate diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 25854b26105..97c9e29ac8f 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -251,7 +251,7 @@ def test_datahandler__check_empty_df(testdatadir, caplog): # @pytest.mark.parametrize('datahandler', []) @pytest.mark.skip("All datahandlers currently support trades data.") def test_datahandler_trades_not_supported(datahandler, testdatadir, ): - # Currently disabled. Reenable should a new provider not support trades data. + # Currently disabled. Re-enable should a new provider not support trades data. dh = get_datahandler(testdatadir, datahandler) with pytest.raises(NotImplementedError): dh.trades_load('UNITTEST/ETH') diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 575d2903b66..f9c56b62b95 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -30,7 +30,7 @@ def test_dp_ohlcv(mocker, default_conf, ohlcv_history, candle_type): assert dp.ohlcv("UNITTEST/BTC", timeframe, candle_type=candletype) is not ohlcv_history assert dp.ohlcv("UNITTEST/BTC", timeframe, copy=False, candle_type=candletype) is ohlcv_history assert not dp.ohlcv("UNITTEST/BTC", timeframe, candle_type=candletype).empty - assert dp.ohlcv("NONESENSE/AAA", timeframe, candle_type=candletype).empty + assert dp.ohlcv("NONSENSE/AAA", timeframe, candle_type=candletype).empty # Test with and without parameter assert dp.ohlcv( @@ -114,7 +114,7 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type): assert dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type) is not ohlcv_history assert not dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type).empty - assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe, candle_type=candle_type).empty + assert dp.get_pair_dataframe("NONSENSE/AAA", timeframe, candle_type=candle_type).empty # Test with and without parameter assert dp.get_pair_dataframe("UNITTEST/BTC", timeframe, candle_type=candle_type)\ @@ -125,7 +125,7 @@ def test_get_pair_dataframe(mocker, default_conf, ohlcv_history, candle_type): assert dp.runmode == RunMode.LIVE assert isinstance(dp.get_pair_dataframe( "UNITTEST/BTC", timeframe, candle_type=candle_type), DataFrame) - assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe, candle_type=candle_type).empty + assert dp.get_pair_dataframe("NONSENSE/AAA", timeframe, candle_type=candle_type).empty historymock = MagicMock(return_value=ohlcv_history) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 4829dd035f2..53840b190de 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -226,8 +226,10 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): assert edge.calculate(edge_conf['exchange']['pair_whitelist']) is False -def mocked_load_data(datadir, pairs=[], timeframe='0m', +def mocked_load_data(datadir, pairs=None, timeframe='0m', timerange=None, *args, **kwargs): + if pairs is None: + pairs = [] hz = 0.1 base = 0.001 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 691ee0b8771..c8293965a3e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -90,7 +90,7 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(TemporaryError): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.OperationFailed("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries @@ -3830,7 +3830,7 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name): [ ("BTC/USDT", 'BTC', 'USDT', "binance", True, False, False, 'spot', {}, True), ("USDT/BTC", 'USDT', 'BTC', "binance", True, False, False, 'spot', {}, True), - # No seperating / + # No separating / ("BTCUSDT", 'BTC', 'USDT', "binance", True, False, False, 'spot', {}, True), ("BTCUSDT", None, "USDT", "binance", True, False, False, 'spot', {}, False), ("USDT/BTC", "BTC", None, "binance", True, False, False, 'spot', {}, False), @@ -4346,7 +4346,7 @@ def test_combine_funding_and_mark( ('binance', 0, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.00091409999), ('binance', 0, 2, "2021-08-31 23:58:00", "2021-09-01 08:00:00", 30.0, -0.00091409999), ('binance', 0, 2, "2021-09-01 00:10:01", "2021-09-01 08:00:00", 30.0, -0.0002493), - # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee + # TODO: Uncomment once _calculate_funding_fees can pass time_in_ratio to exchange. # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0014937), # ('kraken', "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0008289), # ('kraken', "2021-09-01 01:00:14", "2021-09-01 08:00:00", 30.0, -0.0008289), @@ -4358,7 +4358,7 @@ def test_combine_funding_and_mark( ('gate', 0, 2, "2021-09-01 00:00:00", "2021-09-01 12:00:00", 30.0, -0.0009140999), ('gate', 1, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00", 30.0, -0.0002493), ('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0015235), - # TODO: Uncoment once _calculate_funding_fees can pas time_in_ratio to exchange._get_funding_fee + # TODO: Uncomment once _calculate_funding_fees can pass time_in_ratio to exchange. # ('kraken', "2021-09-01 00:00:00", "2021-09-01 08:00:00", 50.0, -0.0024895), ]) def test__fetch_and_calculate_funding_fees( @@ -5133,7 +5133,7 @@ def test_get_maintenance_ratio_and_amt( mocker.patch(f'{EXMS}.exchange_has', return_value=True) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange._leverage_tiers = leverage_tiers - exchange.get_maintenance_ratio_and_amt(pair, value) == (mmr, maintAmt) + assert exchange.get_maintenance_ratio_and_amt(pair, value) == (mmr, maintAmt) def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): diff --git a/tests/exchange/test_exchange_utils.py b/tests/exchange/test_exchange_utils.py index dd79bf083b4..8ae81f81433 100644 --- a/tests/exchange/test_exchange_utils.py +++ b/tests/exchange/test_exchange_utils.py @@ -50,7 +50,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test an available exchange, supported by ccxt default_conf.get('exchange').update({'name': 'huobijp'}) assert check_exchange(default_conf) - assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, " + assert log_has_re(r"Exchange .* is known to the ccxt library, available for the bot, " r"but not officially supported " r"by the Freqtrade development team\. .*", caplog) caplog.clear() @@ -65,7 +65,7 @@ def test_check_exchange(default_conf, caplog) -> None: # Test a 'bad' exchange with check_for_bad=False default_conf.get('exchange').update({'name': 'bitmex'}) assert check_exchange(default_conf, False) - assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, " + assert log_has_re(r"Exchange .* is known to the ccxt library, available for the bot, " r"but not officially supported " r"by the Freqtrade development team\. .*", caplog) caplog.clear() diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 73f87774e66..69e7e498b31 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -472,7 +472,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmp_path, caplog exchange.load_leverage_tiers() assert not log_has(logmsg, caplog) - api_mock.fetch_market_leverage_tiers.call_count == 0 + assert api_mock.fetch_market_leverage_tiers.call_count == 0 # 2 day passes ... time_machine.move_to(datetime.now() + timedelta(weeks=5)) exchange.load_leverage_tiers() @@ -500,7 +500,7 @@ def test__set_leverage_okx(mocker, default_conf): 'posSide': 'net'} api_mock.set_leverage = MagicMock(side_effect=ccxt.NetworkError()) exchange._lev_prep('BTC/USDT:USDT', 3.2, 'buy') - api_mock.fetch_leverage.call_count == 1 + assert api_mock.fetch_leverage.call_count == 1 api_mock.fetch_leverage = MagicMock(side_effect=ccxt.NetworkError()) ccxt_exceptionhandlers( diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index f8cd8f4135b..acd7d747fe4 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -262,6 +262,13 @@ 'leverage_tiers_public': False, 'leverage_in_spot_market': False, }, + 'bingx': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'futures': False, + }, } diff --git a/tests/freqai/test_freqai_backtesting.py b/tests/freqai/test_freqai_backtesting.py index c65934c4e6d..808f37ce52d 100644 --- a/tests/freqai/test_freqai_backtesting.py +++ b/tests/freqai/test_freqai_backtesting.py @@ -133,6 +133,6 @@ def test_freqai_backtest_consistent_timerange(mocker, freqai_conf): backtesting = Backtesting(deepcopy(freqai_conf)) backtesting.start() - gbs.call_args[1]['min_date'] == datetime(2021, 11, 20, 0, 0, tzinfo=timezone.utc) - gbs.call_args[1]['max_date'] == datetime(2021, 11, 21, 0, 0, tzinfo=timezone.utc) + assert gbs.call_args[1]['min_date'] == datetime(2021, 11, 20, 0, 0, tzinfo=timezone.utc) + assert gbs.call_args[1]['max_date'] == datetime(2021, 11, 21, 0, 0, tzinfo=timezone.utc) Backtesting.cleanup() diff --git a/tests/freqai/test_freqai_datadrawer.py b/tests/freqai/test_freqai_datadrawer.py index 7e1a1c32e98..548fad6509b 100644 --- a/tests/freqai/test_freqai_datadrawer.py +++ b/tests/freqai/test_freqai_datadrawer.py @@ -143,7 +143,7 @@ def test_get_timerange_from_backtesting_live_df_pred_not_found(mocker, freqai_co def test_set_initial_return_values(mocker, freqai_conf): """ Simple test of the set initial return values that ensures - we are concatening and ffilling values properly. + we are concatenating and ffilling values properly. """ strategy = get_patched_freqai_strategy(mocker, freqai_conf) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index cd07b913bf7..17898481848 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -403,7 +403,7 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog): freqai.dk.get_unique_classes_from_labels(df) freqai.dk.pair = "ADA/BTC" freqai.dk.full_df = df.fillna(0) - freqai.dk.full_df + assert "&-s_close_mean" not in freqai.dk.full_df.columns assert "&-s_close_std" not in freqai.dk.full_df.columns freqai.backtesting_fit_live_predictions(freqai.dk) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index 0106e1b77c4..993091068a0 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -285,7 +285,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, 'last': enter_price * buy_price_mult, }) - # stoploss shoud be hit + # stoploss should be hit assert freqtrade.handle_trade(trade) is not ignore_strat_sl if not ignore_strat_sl: assert log_has_re('Exit for NEO/BTC detected. Reason: stop_loss.*', caplog) @@ -1398,7 +1398,7 @@ def test_update_trade_state_sell( assert order.status == 'open' freqtrade.update_trade_state(trade, trade.open_orders_ids[-1], l_order) assert trade.amount == l_order['amount'] - # Wallet needs to be updated after closing a limit-sell order to reenable buying + # Wallet needs to be updated after closing a limit-sell order to re-enable buying assert wallet_mock.call_count == 1 assert not trade.is_open # Order is updated by update_trade_state @@ -3122,7 +3122,7 @@ def test_exit_profit_only( if profit_only: assert freqtrade.handle_trade(trade) is False # Custom-exit is called - freqtrade.strategy.custom_exit.call_count == 1 + assert freqtrade.strategy.custom_exit.call_count == 1 patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short) assert freqtrade.handle_trade(trade) is handle_first @@ -3240,7 +3240,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee, ) trade.close(ticker_usdt_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair, side='*') - # Boths sides are locked + # Both sides are locked assert freqtrade.strategy.is_pair_locked(trade.pair, side='long') assert freqtrade.strategy.is_pair_locked(trade.pair, side='short') @@ -4558,6 +4558,67 @@ def test_handle_onexchange_order(mocker, default_conf_usdt, limit_order, is_shor assert trade.exit_reason == ExitType.SOLD_ON_EXCHANGE.value +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize("is_short", [False, True]) +@pytest.mark.parametrize("factor,adjusts", [ + (0.99, True), + (0.97, False), +]) +def test_handle_onexchange_order_changed_amount( + mocker, default_conf_usdt, limit_order, is_short, caplog, + factor, adjusts, +): + default_conf_usdt['dry_run'] = False + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + mock_uts = mocker.spy(freqtrade, 'update_trade_state') + + entry_order = limit_order[entry_side(is_short)] + mock_fo = mocker.patch(f'{EXMS}.fetch_orders', return_value=[ + entry_order, + ]) + + trade = Trade( + pair='ETH/USDT', + fee_open=0.001, + base_currency='ETH', + fee_close=0.001, + open_rate=entry_order['price'], + open_date=dt_now(), + stake_amount=entry_order['cost'], + amount=entry_order['amount'], + exchange="binance", + is_short=is_short, + leverage=1, + ) + freqtrade.wallets = MagicMock() + freqtrade.wallets.get_total = MagicMock(return_value=entry_order['amount'] * factor) + + trade.orders.append(Order.parse_from_ccxt_object( + entry_order, 'ADA/USDT', entry_side(is_short)) + ) + Trade.session.add(trade) + + # assert trade.amount > entry_order['amount'] + + freqtrade.handle_onexchange_order(trade) + assert mock_uts.call_count == 1 + assert mock_fo.call_count == 1 + + trade = Trade.session.scalars(select(Trade)).first() + + assert log_has_re(r'.*has a total of .* but the Wallet shows.*', caplog) + if adjusts: + # Trade amount is updated + assert trade.amount == entry_order['amount'] * factor + assert log_has_re(r'.*Adjusting trade amount to.*', caplog) + else: + assert log_has_re(r'.*Refusing to adjust as the difference.*', caplog) + assert trade.amount == entry_order['amount'] + + assert len(trade.orders) == 1 + assert trade.is_open is True + + @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize("is_short", [False, True]) def test_handle_onexchange_order_exit(mocker, default_conf_usdt, limit_order, is_short, caplog): @@ -4829,7 +4890,7 @@ def refresh_latest_ohlcv_mock(pairlist, **kwargs): freqtrade.execute_entry('ETH/USDT', 123, is_short=is_short) freqtrade.execute_entry('LTC/USDT', 2.0, is_short=is_short) freqtrade.execute_entry('XRP/USDT', 123, is_short=is_short) - multipl = 1 if is_short else -1 + multiple = 1 if is_short else -1 trades = Trade.get_open_trades() assert len(trades) == 3 for trade in trades: @@ -4847,7 +4908,7 @@ def refresh_latest_ohlcv_mock(pairlist, **kwargs): assert trade.funding_fees == pytest.approx(sum( trade.amount * mark_prices[trade.pair].iloc[1:2]['open'] * - funding_rates[trade.pair].iloc[1:2]['open'] * multipl + funding_rates[trade.pair].iloc[1:2]['open'] * multiple )) else: @@ -4859,7 +4920,7 @@ def refresh_latest_ohlcv_mock(pairlist, **kwargs): trade.amount * mark_prices[trade.pair].iloc[1:2]['open'] * funding_rates[trade.pair].iloc[1:2]['open'] * - multipl + multiple )) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 71cb8ff3414..54468910c42 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -107,7 +107,7 @@ trades=[BTrade(exit_reason=ExitType.ROI, open_tick=1, close_tick=3)] ) -# Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss +# Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positive, Stop-Loss triggers 2% Loss # stop-loss: 2% ROI: 5% tc6 = BTContainer(data=[ # D O H L C V EL XL ES Xs BT @@ -121,7 +121,7 @@ trades=[BTrade(exit_reason=ExitType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain +# Test 7: 6% Positive / 1% Negative / Close 1% Positive, ROI Triggers 3% Gain # stop-loss: 2% ROI: 3% tc7 = BTContainer(data=[ # D O H L C V EL XL ES Xs BT diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8fc42b47492..4163e9606d1 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -1473,7 +1473,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) text_table_mock = MagicMock() - sell_reason_mock = MagicMock() + tag_metrics_mock = MagicMock() strattable_mock = MagicMock() strat_summary = MagicMock() @@ -1483,7 +1483,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): ) mocker.patch.multiple('freqtrade.optimize.optimize_reports.optimize_reports', generate_pair_metrics=MagicMock(), - generate_exit_reason_stats=sell_reason_mock, + generate_tag_metrics=tag_metrics_mock, generate_strategy_comparison=strat_summary, generate_daily_stats=MagicMock(), ) @@ -1508,7 +1508,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): assert backtestmock.call_count == 2 assert text_table_mock.call_count == 4 assert strattable_mock.call_count == 1 - assert sell_reason_mock.call_count == 2 + assert tag_metrics_mock.call_count == 4 assert strat_summary.call_count == 1 # check the logs, that will contain the backtest result diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 2a158acf3b2..983e4b47fd3 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -87,9 +87,9 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> for _, t in results.iterrows(): ln = data_pair.loc[data_pair["date"] == t["open_date"]] - # Check open trade rate alignes to open rate + # Check open trade rate aligns to open rate assert ln is not None - # check close trade rate alignes to close rate or is between high and low + # check close trade rate aligns to close rate or is between high and low ln = data_pair.loc[data_pair["date"] == t["close_date"]] assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or round(ln.iloc[0]["low"], 6) < round( @@ -121,8 +121,8 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) pair = 'XRP/USDT:USDT' - row = [ - pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + row_enter = [ + pd.Timestamp(year=2020, month=1, day=1, hour=4, minute=0), 2.1, # Open 2.2, # High 1.9, # Low @@ -133,10 +133,24 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera 0, # exit_short '', # enter_tag '', # exit_tag - ] + ] + # Exit row - with slightly different values + row_exit = [ + pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0), + 2.2, # Open + 2.3, # High + 2.0, # Low + 2.2, # Close + 1, # enter_long + 0, # exit_long + 0, # enter_short + 0, # exit_short + '', # enter_tag + '', # exit_tag + ] backtesting.strategy.leverage = MagicMock(return_value=leverage) - trade = backtesting._enter_trade(pair, row=row, direction='long') - current_time = row[0].to_pydatetime() + trade = backtesting._enter_trade(pair, row=row_enter, direction='long') + current_time = row_enter[0].to_pydatetime() assert trade assert pytest.approx(trade.stake_amount) == 100.0 assert pytest.approx(trade.amount) == 47.61904762 * leverage @@ -144,7 +158,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera backtesting.strategy.adjust_trade_position = MagicMock(return_value=None) assert pytest.approx(trade.liquidation_price) == (0.10278333 if leverage == 1 else 1.2122249) - trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row_enter, current_time) assert trade assert pytest.approx(trade.stake_amount) == 100.0 assert pytest.approx(trade.amount) == 47.61904762 * leverage @@ -152,30 +166,32 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera # Increase position by 100 backtesting.strategy.adjust_trade_position = MagicMock(return_value=(100, 'PartIncrease')) - trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row_enter, current_time) + liq_price = 0.1038916 if leverage == 1 else 1.2127791 assert trade assert pytest.approx(trade.stake_amount) == 200.0 assert pytest.approx(trade.amount) == 95.23809524 * leverage assert len(trade.orders) == 2 assert trade.orders[-1].ft_order_tag == 'PartIncrease' - assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791) + assert pytest.approx(trade.liquidation_price) == liq_price # Reduce by more than amount - no change to trade. backtesting.strategy.adjust_trade_position = MagicMock(return_value=-500) + current_time = row_exit[0].to_pydatetime() - trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row_exit, current_time) assert trade assert pytest.approx(trade.stake_amount) == 200.0 assert pytest.approx(trade.amount) == 95.23809524 * leverage assert len(trade.orders) == 2 assert trade.nr_of_successful_entries == 2 - assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791) + assert pytest.approx(trade.liquidation_price) == liq_price # Reduce position by 50 backtesting.strategy.adjust_trade_position = MagicMock(return_value=(-100, 'partDecrease')) - trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row_exit, current_time) assert trade assert pytest.approx(trade.stake_amount) == 100.0 @@ -184,11 +200,11 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera assert trade.orders[-1].ft_order_tag == 'partDecrease' assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_exits == 1 - assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791) + assert pytest.approx(trade.liquidation_price) == liq_price # Adjust below minimum backtesting.strategy.adjust_trade_position = MagicMock(return_value=-99) - trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time) + trade = backtesting._get_adjust_trade_entry_for_candle(trade, row_exit, current_time) assert trade assert pytest.approx(trade.stake_amount) == 100.0 @@ -196,4 +212,4 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera assert len(trade.orders) == 3 assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_exits == 1 - assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791) + assert pytest.approx(trade.liquidation_price) == liq_price diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index be27bb3f51b..a68a0fc39f3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -901,6 +901,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None hyperopt.get_optimizer([], 2) +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None: mocker.patch(f'{EXMS}.validate_config', MagicMock()) mocker.patch(f'{EXMS}.get_fee', fee) diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index c4689737446..47aba6b76b5 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -172,8 +172,8 @@ def test__pprint_dict(): }""" -def test_get_strategy_filename(default_conf): - +def test_get_strategy_filename(default_conf, tmp_path): + default_conf['user_data_dir'] = tmp_path x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV3') assert isinstance(x, Path) assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v3.py' @@ -233,6 +233,7 @@ def test_export_params(tmp_path): def test_try_export_params(default_conf, tmp_path, caplog, mocker): default_conf['disableparamexport'] = False + default_conf['user_data_dir'] = tmp_path export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params") filename = tmp_path / f"{CURRENT_TEST_STRATEGY}.json" diff --git a/tests/optimize/test_lookahead_analysis.py b/tests/optimize/test_lookahead_analysis.py index d7c4bc6facd..6c84663b6ce 100644 --- a/tests/optimize/test_lookahead_analysis.py +++ b/tests/optimize/test_lookahead_analysis.py @@ -14,7 +14,8 @@ @pytest.fixture -def lookahead_conf(default_conf_usdt): +def lookahead_conf(default_conf_usdt, tmp_path): + default_conf_usdt['user_data_dir'] = tmp_path default_conf_usdt['minimum_trade_amount'] = 10 default_conf_usdt['targeted_trade_amount'] = 20 default_conf_usdt['timerange'] = '20220101-20220501' @@ -152,7 +153,7 @@ def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf assert data[0][2].__contains__('too few trades') assert len(data[0]) == 3 - # now check for an error which occured after enough trades + # now check for an error which occurred after enough trades analysis.total_signals = 12 analysis.false_entry_signals = 11 analysis.false_exit_signals = 10 diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 57605d038e5..f38fcb8855f 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -15,16 +15,16 @@ from freqtrade.edge import PairInfo from freqtrade.enums import ExitType from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, - generate_edge_table, generate_exit_reason_stats, - generate_pair_metrics, + generate_edge_table, generate_pair_metrics, generate_periodic_breakdown_stats, generate_strategy_comparison, generate_trading_stats, show_sorted_pairlist, store_backtest_analysis_results, store_backtest_stats, text_table_bt_results, - text_table_exit_reason, text_table_strategy) + text_table_strategy) +from freqtrade.optimize.optimize_reports.bt_output import text_table_tags from freqtrade.optimize.optimize_reports.optimize_reports import (_get_resample_from_period, - calc_streak) + calc_streak, generate_tag_metrics) from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.util import dt_ts from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc @@ -129,7 +129,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path): assert strat_stats['backtest_start'] == min_date.strftime(DATETIME_PRINT_FORMAT) assert strat_stats['backtest_end'] == max_date.strftime(DATETIME_PRINT_FORMAT) assert strat_stats['total_trades'] == len(results['DefStrat']['results']) - # Above sample had no loosing trade + # Above sample had no losing trade assert strat_stats['max_drawdown_account'] == 0.0 # Retry with losing trade @@ -229,6 +229,28 @@ def test_store_backtest_stats(testdatadir, mocker): assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult')) +def test_store_backtest_stats_real(tmp_path): + data = {'metadata': {}, 'strategy': {}, 'strategy_comparison': []} + store_backtest_stats(tmp_path, data, '2022_01_01_15_05_13') + + assert (tmp_path / 'backtest-result-2022_01_01_15_05_13.json').is_file() + assert (tmp_path / 'backtest-result-2022_01_01_15_05_13.meta.json').is_file() + assert not (tmp_path / 'backtest-result-2022_01_01_15_05_13_market_change.feather').is_file() + assert (tmp_path / LAST_BT_RESULT_FN).is_file() + fn = get_latest_backtest_filename(tmp_path) + assert fn == 'backtest-result-2022_01_01_15_05_13.json' + + store_backtest_stats(tmp_path, data, '2024_01_01_15_05_25', market_change_data=pd.DataFrame()) + assert (tmp_path / 'backtest-result-2024_01_01_15_05_25.json').is_file() + assert (tmp_path / 'backtest-result-2024_01_01_15_05_25.meta.json').is_file() + assert (tmp_path / 'backtest-result-2024_01_01_15_05_25_market_change.feather').is_file() + assert (tmp_path / LAST_BT_RESULT_FN).is_file() + + # Last file reference should be updated + fn = get_latest_backtest_filename(tmp_path) + assert fn == 'backtest-result-2024_01_01_15_05_25.json' + + def test_store_backtest_candles(testdatadir, mocker): dump_mock = mocker.patch( @@ -392,20 +414,21 @@ def test_text_table_exit_reason(): ) result_str = ( - '| Exit Reason | Exits | Win Draws Loss Win% | Avg Profit % |' - ' Tot Profit BTC | Tot Profit % |\n' - '|---------------+---------+--------------------------+----------------+' - '------------------+----------------|\n' - '| roi | 2 | 2 0 0 100 | 15 |' - ' 0.6 | 15 |\n' - '| stop_loss | 1 | 0 0 1 0 | -10 |' - ' -0.2 | -5 |' + '| Exit Reason | Exits | Avg Profit % | Tot Profit BTC | Tot Profit % |' + ' Avg Duration | Win Draw Loss Win% |\n' + '|---------------+---------+----------------+------------------+----------------+' + '----------------+-------------------------|\n' + '| roi | 2 | 15.00 | 0.60000000 | 2.73 |' + ' 0:20:00 | 2 0 0 100 |\n' + '| stop_loss | 1 | -10.00 | -0.20000000 | -0.91 |' + ' 0:10:00 | 0 0 1 0 |\n' + '| TOTAL | 3 | 6.67 | 0.40000000 | 1.82 |' + ' 0:17:00 | 2 0 1 66.7 |' ) - exit_reason_stats = generate_exit_reason_stats(max_open_trades=2, - results=results) - assert text_table_exit_reason(exit_reason_stats=exit_reason_stats, - stake_currency='BTC') == result_str + exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=22, + results=results, skip_nan=False) + assert text_table_tags('exit_tag', exit_reason_stats, 'BTC') == result_str def test_generate_sell_reason_stats(): @@ -423,10 +446,10 @@ def test_generate_sell_reason_stats(): } ) - exit_reason_stats = generate_exit_reason_stats(max_open_trades=2, - results=results) + exit_reason_stats = generate_tag_metrics('exit_reason', starting_balance=22, + results=results, skip_nan=False) roi_result = exit_reason_stats[0] - assert roi_result['exit_reason'] == 'roi' + assert roi_result['key'] == 'roi' assert roi_result['trades'] == 2 assert pytest.approx(roi_result['profit_mean']) == 0.15 assert roi_result['profit_mean_pct'] == round(roi_result['profit_mean'] * 100, 2) @@ -435,7 +458,7 @@ def test_generate_sell_reason_stats(): stop_result = exit_reason_stats[1] - assert stop_result['exit_reason'] == 'stop_loss' + assert stop_result['key'] == 'stop_loss' assert stop_result['trades'] == 1 assert pytest.approx(stop_result['profit_mean']) == -0.1 assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) diff --git a/tests/optimize/test_recursive_analysis.py b/tests/optimize/test_recursive_analysis.py index 33fae0d085e..95a96c9f5b1 100644 --- a/tests/optimize/test_recursive_analysis.py +++ b/tests/optimize/test_recursive_analysis.py @@ -14,7 +14,8 @@ @pytest.fixture -def recursive_conf(default_conf_usdt): +def recursive_conf(default_conf_usdt, tmp_path): + default_conf_usdt['user_data_dir'] = tmp_path default_conf_usdt['timerange'] = '20220101-20220501' default_conf_usdt['strategy_path'] = str( diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index a6a107a5ec1..d354e8f2292 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -436,8 +436,8 @@ def test_migrate_pairlocks(mocker, default_conf, fee, caplog): assert len(PairLock.session.scalars(select(PairLock).filter(PairLock.pair == '*')).all()) == 1 pairlocks = PairLock.session.scalars(select(PairLock).filter(PairLock.pair == 'ETH/BTC')).all() assert len(pairlocks) == 1 - pairlocks[0].pair == 'ETH/BTC' - pairlocks[0].side == '*' + assert pairlocks[0].pair == 'ETH/BTC' + assert pairlocks[0].side == '*' @pytest.mark.parametrize('dialect', [ diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 18f28da2be7..a9c27a9b52a 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1871,7 +1871,7 @@ def test_get_trades__query(fee, is_short): # without orders there should be no join issued. query1 = Trade.get_trades_query([], include_orders=False) - # Empty "with-options -> default - selectin" + # Empty "with-options -> default - selection" assert query._with_options == () assert query1._with_options != () diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 39f48f4545b..1a23846b789 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -433,7 +433,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter, no seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], - "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist + "USDT", 4), # whitelist_result is integer -- check only length of randomized pairlist # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected @@ -565,7 +565,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t if isinstance(whitelist_result, list): assert whitelist == whitelist_result else: - len(whitelist) == whitelist_result + assert len(whitelist) == whitelist_result for pairlist in pairlists: if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ @@ -605,7 +605,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "lookback_days": 1}], "BTC", "binance", "default_refresh_too_short"), # OperationalException expected - # ambigous configuration with lookback days and period + # ambiguous configuration with lookback days and period ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "lookback_days": 1, "lookback_period": 1}], "BTC", "binance", "lookback_days_and_period"), # OperationalException expected @@ -617,7 +617,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "lookback_timeframe": "1m", "lookback_period": 2000, "refresh_period": 3600}], "BTC", "binance", "lookback_exceeds_exchange_request_size"), # OperationalException expected - # expecing pairs as given + # expecting pairs as given ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], "BTC", "binance", ['LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC', 'HOT/BTC']), diff --git a/tests/rpc/test_fiat_convert.py b/tests/rpc/test_fiat_convert.py index 0dfa2018565..717866cfd37 100644 --- a/tests/rpc/test_fiat_convert.py +++ b/tests/rpc/test_fiat_convert.py @@ -90,7 +90,7 @@ def test_loadcryptomap(mocker): fiat_convert = CryptoToFiatConverter() assert len(fiat_convert._coinlistings) == 2 - assert fiat_convert._get_gekko_id("btc") == "bitcoin" + assert fiat_convert._get_gecko_id("btc") == "bitcoin" def test_fiat_init_network_exception(mocker): @@ -109,16 +109,16 @@ def test_fiat_init_network_exception(mocker): def test_fiat_convert_without_network(mocker): - # Because CryptoToFiatConverter is a Singleton we reset the value of _coingekko + # Because CryptoToFiatConverter is a Singleton we reset the value of _coingecko fiat_convert = CryptoToFiatConverter() - cmc_temp = CryptoToFiatConverter._coingekko - CryptoToFiatConverter._coingekko = None + cmc_temp = CryptoToFiatConverter._coingecko + CryptoToFiatConverter._coingecko = None - assert fiat_convert._coingekko is None + assert fiat_convert._coingecko is None assert fiat_convert._find_price(crypto_symbol='btc', fiat_symbol='usd') == 0.0 - CryptoToFiatConverter._coingekko = cmc_temp + CryptoToFiatConverter._coingecko = cmc_temp def test_fiat_too_many_requests_response(mocker, caplog): @@ -152,9 +152,9 @@ def test_fiat_multiple_coins(mocker, caplog): {'id': 'ethereum-wormhole', 'symbol': 'eth', 'name': 'Ethereum Wormhole'}, ] - assert fiat_convert._get_gekko_id('btc') == 'bitcoin' - assert fiat_convert._get_gekko_id('hnt') is None - assert fiat_convert._get_gekko_id('eth') == 'ethereum' + assert fiat_convert._get_gecko_id('btc') == 'bitcoin' + assert fiat_convert._get_gecko_id('hnt') is None + assert fiat_convert._get_gecko_id('eth') == 'ethereum' assert log_has('Found multiple mappings in CoinGecko for hnt.', caplog) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0bf39fab75c..5c8602c2f58 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -11,7 +11,6 @@ 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 from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt, @@ -222,7 +221,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers assert "Pair" in headers - assert 'instantly' == result[0][2] + assert 'now' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '0.00 (0.00)' == result[0][3] assert '0.00' == f'{fiat_profit_sum:.2f}' @@ -233,7 +232,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers assert "Pair" in headers - assert 'instantly' == result[0][2] + assert 'now' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '-0.41% (-0.00)' == result[0][3] assert '-0.00' == f'{fiat_profit_sum:.2f}' @@ -244,7 +243,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert "Since" in headers assert "Pair" in headers assert len(result[0]) == 4 - assert 'instantly' == result[0][2] + assert 'now' == result[0][2] assert 'ETH/BTC' in result[0][1] assert '-0.41% (-0.06)' == result[0][3] assert '-0.06' == f'{fiat_profit_sum:.2f}' @@ -261,7 +260,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: mocker.patch(f'{EXMS}.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') - assert 'instantly' == result[0][2] + assert 'now' == result[0][2] assert 'ETH/BTC' in result[0][1] assert 'nan%' == result[0][3] assert isnan(fiat_profit_sum) @@ -1171,14 +1170,15 @@ def test_rpc_force_entry_wrong_mode(mocker, default_conf) -> None: @pytest.mark.usefixtures("init_persistence") -def test_rpc_delete_lock(mocker, default_conf): +def test_rpc_add_and_delete_lock(mocker, default_conf): freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) pair = 'ETH/BTC' - PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=4)) - PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=5)) - PairLocks.lock_pair(pair, datetime.now(timezone.utc) + timedelta(minutes=10)) + rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=4), '', '*') + rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=5), '', '*') + rpc._rpc_add_lock(pair, datetime.now(timezone.utc) + timedelta(minutes=10), '', '*') + locks = rpc._rpc_locks() assert locks['lock_count'] == 3 locks1 = rpc._rpc_delete_lock(lockid=locks['locks'][0]['id']) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 1e008d98e7a..fd45cc84a78 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -23,12 +23,13 @@ from freqtrade.exceptions import DependencyException, ExchangeError, OperationalException from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.optimize.backtesting import Backtesting -from freqtrade.persistence import PairLocks, Trade +from freqtrade.persistence import Trade from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer from freqtrade.rpc.api_server.webserver_bgwork import ApiBG +from freqtrade.util.datetime_helpers import format_date from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) @@ -71,8 +72,10 @@ def botclient(default_conf, mocker): ApiServer.shutdown() -def client_post(client: TestClient, url, data={}): +def client_post(client: TestClient, url, data=None): + if data is None: + data = {} return client.post(url, json=data, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), @@ -81,8 +84,10 @@ def client_post(client: TestClient, url, data={}): }) -def client_patch(client: TestClient, url, data={}): +def client_patch(client: TestClient, url, data=None): + if data is None: + data = {} return client.patch(url, json=data, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), @@ -553,8 +558,19 @@ def test_api_locks(botclient): assert rc.json()['lock_count'] == 0 assert rc.json()['lock_count'] == len(rc.json()['locks']) - PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason') - PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef') + rc = client_post(client, f"{BASE_URI}/locks", [ + { + "pair": "ETH/BTC", + "until": f"{format_date(datetime.now(timezone.utc) + timedelta(minutes=4))}Z", + "reason": "randreason" + }, { + "pair": "XRP/BTC", + "until": f"{format_date(datetime.now(timezone.utc) + timedelta(minutes=20))}Z", + "reason": "deadbeef" + } + ]) + assert_response(rc) + assert rc.json()['lock_count'] == 2 rc = client_get(client, f"{BASE_URI}/locks") assert_response(rc) @@ -1495,6 +1511,7 @@ def test_api_pair_candles(botclient, ohlcv_history): assert 'data_stop_ts' in rc.json() assert len(rc.json()['data']) == 0 ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() + ohlcv_history['sma2'] = ohlcv_history['close'].rolling(2).mean() ohlcv_history['enter_long'] = 0 ohlcv_history.loc[1, 'enter_long'] = 1 ohlcv_history['exit_long'] = 0 @@ -1502,44 +1519,83 @@ def test_api_pair_candles(botclient, ohlcv_history): ohlcv_history['exit_short'] = 0 ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history, CandleType.SPOT) + for call in ('get', 'post'): + if call == 'get': + rc = client_get( + client, + f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") + else: + rc = client_post( + client, + f"{BASE_URI}/pair_candles", + data={ + "pair": "XRP/BTC", + "timeframe": timeframe, + "limit": amount, + "columns": ['sma'], + } + ) + assert_response(rc) + resp = rc.json() + assert 'strategy' in resp + assert resp['strategy'] == CURRENT_TEST_STRATEGY + assert 'columns' in resp + assert 'data_start_ts' in resp + assert 'data_start' in resp + assert 'data_stop' in resp + assert 'data_stop_ts' in resp + assert resp['data_start'] == '2017-11-26 08:50:00+00:00' + assert resp['data_start_ts'] == 1511686200000 + assert resp['data_stop'] == '2017-11-26 09:00:00+00:00' + assert resp['data_stop_ts'] == 1511686800000 + assert isinstance(resp['columns'], list) + base_cols = { + 'date', 'open', 'high', 'low', 'close', 'volume', + 'sma', 'enter_long', 'exit_long', 'enter_short', 'exit_short', '__date_ts', + '_enter_long_signal_close', '_exit_long_signal_close', + '_enter_short_signal_close', '_exit_short_signal_close' + } + if call == 'get': + assert set(resp['columns']) == base_cols.union({'sma2'}) + else: + assert set(resp['columns']) == base_cols + + # All columns doesn't include the internal columns + assert set(resp['all_columns']) == { + 'date', 'open', 'high', 'low', 'close', 'volume', + 'sma', 'sma2', 'enter_long', 'exit_long', 'enter_short', 'exit_short' + } + assert 'pair' in resp + assert resp['pair'] == 'XRP/BTC' + + assert 'data' in resp + assert len(resp['data']) == amount + if call == 'get': + assert len(resp['data'][0]) == 17 + assert resp['data'] == [ + ['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, + 0.0877869, None, None, 0, 0, 0, 0, 1511686200000, None, None, None, None], + ['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05, 8.893e-05, 0.05874751, + 8.886500000000001e-05, 8.886500000000001e-05, 1, 0, 0, 0, 1511686500000, + 8.893e-05, None, None, None], + ['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, + 0.7039405, 8.885e-05, 8.885e-05, 0, 0, 0, 0, 1511686800000, None, None, None, None + ] + ] + else: + assert len(resp['data'][0]) == 16 + assert resp['data'] == [ + ['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, + 0.0877869, None, 0, 0, 0, 0, 1511686200000, None, None, None, None], + ['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05, 8.893e-05, 0.05874751, + 8.886500000000001e-05, 1, 0, 0, 0, 1511686500000, + 8.893e-05, None, None, None], + ['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, + 0.7039405, 8.885e-05, 0, 0, 0, 0, 1511686800000, None, None, None, None + ] + ] - rc = client_get(client, - f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") - assert_response(rc) - assert 'strategy' in rc.json() - assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY - assert 'columns' in rc.json() - assert 'data_start_ts' in rc.json() - assert 'data_start' in rc.json() - assert 'data_stop' in rc.json() - assert 'data_stop_ts' in rc.json() - assert rc.json()['data_start'] == '2017-11-26 08:50:00+00:00' - assert rc.json()['data_start_ts'] == 1511686200000 - assert rc.json()['data_stop'] == '2017-11-26 09:00:00+00:00' - assert rc.json()['data_stop_ts'] == 1511686800000 - assert isinstance(rc.json()['columns'], list) - assert set(rc.json()['columns']) == { - 'date', 'open', 'high', 'low', 'close', 'volume', - 'sma', 'enter_long', 'exit_long', 'enter_short', 'exit_short', '__date_ts', - '_enter_long_signal_close', '_exit_long_signal_close', - '_enter_short_signal_close', '_exit_short_signal_close' - } - assert 'pair' in rc.json() - assert rc.json()['pair'] == 'XRP/BTC' - - assert 'data' in rc.json() - assert len(rc.json()['data']) == amount - - assert (rc.json()['data'] == - [['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, - None, 0, 0, 0, 0, 1511686200000, None, None, None, None], - ['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 0, 0, 1511686500000, 8.893e-05, - None, None, None], - ['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, - 0.7039405, 8.885e-05, 0, 0, 0, 0, 1511686800000, None, None, None, None] - - ]) + # prep for next test ohlcv_history['exit_long'] = ohlcv_history['exit_long'].astype('float64') ohlcv_history.at[0, 'exit_long'] = float('inf') ohlcv_history['date1'] = ohlcv_history['date'] @@ -1551,18 +1607,20 @@ def test_api_pair_candles(botclient, ohlcv_history): assert_response(rc) assert (rc.json()['data'] == [['2017-11-26T08:50:00Z', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, - None, 0, None, 0, 0, None, 1511686200000, None, None, None, None], + None, None, 0, None, 0, 0, None, 1511686200000, None, None, None, None], ['2017-11-26T08:55:00Z', 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0.0, 0, 0, '2017-11-26T08:55:00Z', - 1511686500000, 8.893e-05, None, None, None], + 8.893e-05, 0.05874751, 8.886500000000001e-05, 8.886500000000001e-05, 1, 0.0, 0, + 0, '2017-11-26T08:55:00Z', 1511686500000, 8.893e-05, None, None, None], ['2017-11-26T09:00:00Z', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, - 0.7039405, 8.885e-05, 0, 0.0, 0, 0, '2017-11-26T09:00:00Z', 1511686800000, - None, None, None, None] + 0.7039405, 8.885e-05, 8.885e-05, 0, 0.0, 0, 0, '2017-11-26T09:00:00Z', + 1511686800000, None, None, None, None] ]) -def test_api_pair_history(botclient, mocker): +def test_api_pair_history(botclient, tmp_path, mocker): _ftbot, client = botclient + _ftbot.config['user_data_dir'] = tmp_path + timeframe = '5m' lfm = mocker.patch('freqtrade.strategy.interface.IStrategy.load_freqAI_model') # No pair @@ -1596,44 +1654,74 @@ def test_api_pair_history(botclient, mocker): assert_response(rc, 502) # Working - rc = client_get(client, - f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") - assert_response(rc, 200) - result = rc.json() - assert result['length'] == 289 - assert len(result['data']) == result['length'] - assert 'columns' in result - assert 'data' in result - data = result['data'] - assert len(data) == 289 - # analyed DF has 30 columns - assert len(result['columns']) == 30 - assert len(data[0]) == 30 - date_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'date'][0] - rsi_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'rsi'][0] - - assert data[0][date_col_idx] == '2018-01-11T00:00:00Z' - assert data[0][rsi_col_idx] is not None - assert data[0][rsi_col_idx] > 0 - assert lfm.call_count == 1 - assert result['pair'] == 'UNITTEST/BTC' - assert result['strategy'] == CURRENT_TEST_STRATEGY - assert result['data_start'] == '2018-01-11 00:00:00+00:00' - assert result['data_start_ts'] == 1515628800000 - assert result['data_stop'] == '2018-01-12 00:00:00+00:00' - assert result['data_stop_ts'] == 1515715200000 - - # No data found - rc = client_get(client, - f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}") - assert_response(rc, 502) - assert rc.json()['detail'] == ("No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") + for call in ('get', 'post'): + if call == 'get': + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") + else: + rc = client_post( + client, + f"{BASE_URI}/pair_history", + data={ + "pair": "UNITTEST/BTC", + "timeframe": timeframe, + "timerange": "20180111-20180112", + "strategy": CURRENT_TEST_STRATEGY, + "columns": ['rsi', 'fastd', 'fastk'], + }) + + assert_response(rc, 200) + result = rc.json() + assert result['length'] == 289 + assert len(result['data']) == result['length'] + assert 'columns' in result + assert 'data' in result + data = result['data'] + assert len(data) == 289 + col_count = 30 if call == 'get' else 18 + # analyzed DF has 30 columns + assert len(result['columns']) == col_count + assert len(result['all_columns']) == 25 + assert len(data[0]) == col_count + date_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'date'][0] + rsi_col_idx = [idx for idx, c in enumerate(result['columns']) if c == 'rsi'][0] + + assert data[0][date_col_idx] == '2018-01-11T00:00:00Z' + assert data[0][rsi_col_idx] is not None + assert data[0][rsi_col_idx] > 0 + assert lfm.call_count == 1 + assert result['pair'] == 'UNITTEST/BTC' + assert result['strategy'] == CURRENT_TEST_STRATEGY + assert result['data_start'] == '2018-01-11 00:00:00+00:00' + assert result['data_start_ts'] == 1515628800000 + assert result['data_stop'] == '2018-01-12 00:00:00+00:00' + assert result['data_stop_ts'] == 1515715200000 + lfm.reset_mock() + + # No data found + if call == 'get': + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}") + else: + rc = client_post( + client, + f"{BASE_URI}/pair_history", + data={ + "pair": "UNITTEST/BTC", + "timeframe": timeframe, + "timerange": "20200111-20200112", + "strategy": CURRENT_TEST_STRATEGY, + "columns": ['rsi', 'fastd', 'fastk'], + }) + assert_response(rc, 502) + assert rc.json()['detail'] == ("No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") -def test_api_plot_config(botclient, mocker): +def test_api_plot_config(botclient, mocker, tmp_path): ftbot, client = botclient + ftbot.config['user_data_dir'] = tmp_path rc = client_get(client, f"{BASE_URI}/plot_config") assert_response(rc) @@ -1701,8 +1789,9 @@ def test_api_strategies(botclient, tmp_path): ]} -def test_api_strategy(botclient): +def test_api_strategy(botclient, tmp_path, mocker): _ftbot, client = botclient + _ftbot.config['user_data_dir'] = tmp_path rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}") @@ -1718,6 +1807,11 @@ def test_api_strategy(botclient): # Disallow base64 strategies rc = client_get(client, f"{BASE_URI}/strategy/xx:cHJpbnQoImhlbGxvIHdvcmxkIik=") assert_response(rc, 500) + mocker.patch('freqtrade.resolvers.strategy_resolver.StrategyResolver._load_strategy', + side_effect=Exception("Test")) + + rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") + assert_response(rc, 502) def test_api_exchanges(botclient): @@ -2237,6 +2331,42 @@ def read_metadata(): assert fileres[CURRENT_TEST_STRATEGY]['notes'] == 'FooBar' +def test_api_patch_backtest_market_change(botclient, tmp_path: Path): + ftbot, client = botclient + + # Create a temporary directory and file + bt_results_base = tmp_path / "backtest_results" + bt_results_base.mkdir() + file_path = bt_results_base / "test_22_market_change.feather" + df = pd.DataFrame({ + 'date': ['2018-01-01T00:00:00Z', '2018-01-01T00:05:00Z'], + 'count': [2, 4], + 'mean': [2555, 2556], + 'rel_mean': [0, 0.022], + }) + df['date'] = pd.to_datetime(df['date']) + df.to_feather(file_path, compression_level=9, compression='lz4') + # Nonexisting file + rc = client_get(client, f"{BASE_URI}/backtest/history/randomFile.json/market_change") + assert_response(rc, 503) + + ftbot.config['user_data_dir'] = tmp_path + ftbot.config['runmode'] = RunMode.WEBSERVER + + rc = client_get(client, f"{BASE_URI}/backtest/history/randomFile.json/market_change") + assert_response(rc, 404) + + rc = client_get(client, f"{BASE_URI}/backtest/history/test_22/market_change") + assert_response(rc, 200) + result = rc.json() + assert result['length'] == 2 + assert result['columns'] == ['date', 'count', 'mean', 'rel_mean', '__date_ts'] + assert result['data'] == [ + ['2018-01-01T00:00:00Z', 2, 2555, 0.0, 1514764800000], + ['2018-01-01T00:05:00Z', 4, 2556, 0.022, 1514765100000] + ] + + def test_health(botclient): _ftbot, client = botclient diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 3bd372b191e..9a3b713e210 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1821,8 +1821,8 @@ async def test_edge_enabled(edge_conf, update, mocker) -> None: @pytest.mark.parametrize('is_short,regex_pattern', - [(True, r"just now[ ]*XRP\/BTC \(#3\) -1.00% \("), - (False, r"just now[ ]*XRP\/BTC \(#3\) 1.00% \(")]) + [(True, r"now[ ]*XRP\/BTC \(#3\) -1.00% \("), + (False, r"now[ ]*XRP\/BTC \(#3\) 1.00% \(")]) async def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern): telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1846,7 +1846,7 @@ async def test_telegram_trades(mocker, update, default_conf, fee, is_short, rege context = MagicMock() context.args = [5] await telegram._trades(update=update, context=context) - msg_mock.call_count == 1 + assert msg_mock.call_count == 1 assert "2 recent trades:" in msg_mock.call_args_list[0][0][0] assert "Profit (" in msg_mock.call_args_list[0][0][0] assert "Close Date" in msg_mock.call_args_list[0][0][0] @@ -1870,7 +1870,7 @@ async def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short context = MagicMock() context.args = [1] await telegram._delete_trade(update=update, context=context) - msg_mock.call_count == 1 + assert msg_mock.call_count == 1 assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0] assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0] @@ -2032,20 +2032,20 @@ def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type, '*Total:* `0.01465333 BTC / 180.895 USD`' ) - freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} + freqtradebot.config['telegram']['notification_settings'] = {'entry': 'off'} caplog.clear() msg_mock.reset_mock() telegram.send_msg(msg) - msg_mock.call_count == 0 - log_has("Notification 'buy' not sent.", caplog) + assert msg_mock.call_count == 0 + assert log_has("Notification 'entry' not sent.", caplog) - freqtradebot.config['telegram']['notification_settings'] = {'buy': 'silent'} + freqtradebot.config['telegram']['notification_settings'] = {'entry': 'silent'} caplog.clear() msg_mock.reset_mock() telegram.send_msg(msg) - msg_mock.call_count == 1 - msg_mock.call_args_list[0][1]['disable_notification'] is True + assert msg_mock.call_count == 1 + assert msg_mock.call_args_list[0][1]['disable_notification'] is True @pytest.mark.parametrize('message_type,enter_signal', [ @@ -2423,7 +2423,7 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None: telegram.send_msg({ 'type': None, }) - msg_mock.call_count == 0 + assert msg_mock.call_count == 0 @pytest.mark.parametrize('message_type,enter,enter_signal,leverage', [ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dbff49b5779..a53eead95a1 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -81,7 +81,7 @@ def test_returns_latest_signal(ohlcv_history): assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None) _STRATEGY.config['trading_mode'] = 'futures' - # Short signal get's ignored as can_short is not set. + # Short signal gets ignored as can_short is not set. assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None) _STRATEGY.can_short = True diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 22c7359bf46..b7fb7dea1f7 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -253,7 +253,7 @@ def test_stoploss_from_open(side, profitrange): assert stoploss >= 0 # Technically the formula can yield values greater than 1 for shorts - # eventhough it doesn't make sense because the position would be liquidated + # even though it doesn't make sense because the position would be liquidated if side == 'long': assert stoploss <= 1 diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index a31408b5c11..33245cc5f2d 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -78,7 +78,9 @@ def test_load_strategy_base64(dataframe_1m, caplog, default_conf): r".*(/|\\).*(/|\\)SampleStrategy\.py'\.\.\.", caplog) -def test_load_strategy_invalid_directory(caplog, default_conf): +def test_load_strategy_invalid_directory(caplog, default_conf, tmp_path): + default_conf['user_data_dir'] = tmp_path + extra_dir = Path.cwd() / 'some/path' with pytest.raises(OperationalException, match=r"Impossible to load Strategy.*"): StrategyResolver._load_strategy('StrategyTestV333', config=default_conf, @@ -87,7 +89,8 @@ def test_load_strategy_invalid_directory(caplog, default_conf): assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) -def test_load_not_found_strategy(default_conf): +def test_load_not_found_strategy(default_conf, tmp_path): + default_conf['user_data_dir'] = tmp_path default_conf['strategy'] = 'NotFoundStrategy' with pytest.raises(OperationalException, match=r"Impossible to load Strategy 'NotFoundStrategy'. " diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 7f80a8588f8..185bfeaf1b8 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -125,7 +125,7 @@ def test_add_areas(default_conf, testdatadir, caplog): assert fig == fig2 assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) - # everythin given in plot config, row 3 + # everything given in plot config, row 3 fig3 = add_areas(fig, 3, data, indicators) figure = fig3.layout.figure fill_macd = find_trace_in_fig_data(figure.data, "MACD Fill") @@ -495,7 +495,7 @@ def test_plot_profit(default_conf, mocker, testdatadir): # no main_plot, adds empty main_plot ([], [], {'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, {'main_plot': {}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}), - # indicator 1 / 2 should have prevelance + # indicator 1 / 2 should have prevalence (['sma', 'ema3'], ['macd'], {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}} diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 1c1a3b548c9..0d0ada1b7be 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -381,10 +381,10 @@ def test_sync_wallet_futures_dry(mocker, default_conf, fee): assert len(freqtrade.wallets._wallets) == 1 assert len(freqtrade.wallets._positions) == 4 positions = freqtrade.wallets.get_all_positions() - positions['ETH/BTC'].side == 'short' - positions['ETC/BTC'].side == 'long' - positions['XRP/BTC'].side == 'long' - positions['LTC/BTC'].side == 'short' + assert positions['ETH/BTC'].side == 'short' + assert positions['ETC/BTC'].side == 'long' + assert positions['XRP/BTC'].side == 'long' + assert positions['LTC/BTC'].side == 'short' assert freqtrade.wallets.get_starting_balance() == default_conf['dry_run_wallet'] total = freqtrade.wallets.get_total('BTC') diff --git a/tests/utils/test_datetime_helpers.py b/tests/utils/test_datetime_helpers.py index 6fbe752001b..20e6fc0f517 100644 --- a/tests/utils/test_datetime_helpers.py +++ b/tests/utils/test_datetime_helpers.py @@ -3,8 +3,9 @@ import pytest import time_machine -from freqtrade.util import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, dt_ts_def, - dt_ts_none, dt_utc, format_date, format_ms_time, shorten_date) +from freqtrade.util import (dt_floor_day, dt_from_ts, dt_now, dt_ts, dt_ts_def, dt_ts_none, dt_utc, + format_date, format_ms_time, shorten_date) +from freqtrade.util.datetime_helpers import dt_humanize_delta def test_dt_now(): @@ -68,9 +69,12 @@ def test_shorten_date() -> None: def test_dt_humanize() -> None: - assert dt_humanize(dt_now()) == 'just now' - assert dt_humanize(dt_now(), only_distance=True) == 'instantly' - assert dt_humanize(dt_now() - timedelta(hours=16), only_distance=True) == '16 hours' + assert dt_humanize_delta(dt_now()) == 'now' + assert dt_humanize_delta(dt_now() - timedelta(minutes=50)) == '50 minutes ago' + assert dt_humanize_delta(dt_now() - timedelta(hours=16)) == '16 hours ago' + assert dt_humanize_delta(dt_now() - timedelta(hours=16, minutes=30)) == '16 hours ago' + assert dt_humanize_delta(dt_now() - timedelta(days=16, hours=10, minutes=25)) == '16 days ago' + assert dt_humanize_delta(dt_now() - timedelta(minutes=50)) == '50 minutes ago' def test_format_ms_time() -> None: @@ -79,11 +83,12 @@ def test_format_ms_time() -> None: date = format_ms_time(date_in_epoch_ms) assert isinstance(date, str) res = datetime(2018, 4, 10, 18, 2, 1, tzinfo=timezone.utc) - assert date == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S') + assert date == res.strftime('%Y-%m-%dT%H:%M:%S') + assert date == '2018-04-10T18:02:01' res = datetime(2017, 12, 13, 8, 2, 1, tzinfo=timezone.utc) # Date 2017-12-13 08:02:01 date_in_epoch_ms = 1513152121000 - assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S') + assert format_ms_time(date_in_epoch_ms) == res.strftime('%Y-%m-%dT%H:%M:%S') def test_format_date() -> None: diff --git a/tests/utils/test_measure_time.py b/tests/utils/test_measure_time.py new file mode 100644 index 00000000000..dac509907c0 --- /dev/null +++ b/tests/utils/test_measure_time.py @@ -0,0 +1,34 @@ +from unittest.mock import MagicMock + +import time_machine + +from freqtrade.util import MeasureTime + + +def test_measure_time(): + + callback = MagicMock() + with time_machine.travel("2021-09-01 05:00:00 +00:00", tick=False) as t: + + measure = MeasureTime(callback, 5, ttl=60) + with measure: + pass + + assert callback.call_count == 0 + + with measure: + t.shift(10) + + assert callback.call_count == 1 + callback.reset_mock() + with measure: + t.shift(10) + assert callback.call_count == 0 + + callback.reset_mock() + # Shift past the ttl + t.shift(45) + + with measure: + t.shift(10) + assert callback.call_count == 1 diff --git a/tests/utils/test_rendering_utils.py b/tests/utils/test_rendering_utils.py index 7d52b4f265d..e03307ff1b8 100644 --- a/tests/utils/test_rendering_utils.py +++ b/tests/utils/test_rendering_utils.py @@ -7,7 +7,9 @@ def test_render_template_fallback(): from jinja2.exceptions import TemplateNotFound with pytest.raises(TemplateNotFound): val = render_template( - templatefile='subtemplates/indicators_does-not-exist.j2',) + templatefile='subtemplates/indicators_does-not-exist.j2', + arguments={}, + ) val = render_template_with_fallback( templatefile='strategy_subtemplates/indicators_does-not-exist.j2',