diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ceac4a7f..f4b550855 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -461,7 +461,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.7 + uses: pypa/gh-action-pypi-publish@v1.8.8 if: (github.event_name == 'release') with: user: __token__ @@ -469,7 +469,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.7 + uses: pypa/gh-action-pypi-publish@v1.8.8 if: (github.event_name == 'release') with: user: __token__ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a9ca80c21..6e4919763 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,12 +13,12 @@ repos: - id: mypy exclude: build_helpers additional_dependencies: - - types-cachetools==5.3.0.5 + - types-cachetools==5.3.0.6 - types-filelock==3.2.7 - - types-requests==2.31.0.1 - - types-tabulate==0.9.0.2 - - types-python-dateutil==2.8.19.13 - - SQLAlchemy==2.0.18 + - types-requests==2.31.0.2 + - types-tabulate==0.9.0.3 + - types-python-dateutil==2.8.19.14 + - SQLAlchemy==2.0.19 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl deleted file mode 100644 index 466455a8c..000000000 Binary files a/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl deleted file mode 100644 index 16f4f411a..000000000 Binary files a/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl deleted file mode 100644 index 324502fb8..000000000 Binary files a/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl deleted file mode 100644 index 389403041..000000000 Binary files a/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.27-cp310-cp310-win_amd64.whl b/build_helpers/TA_Lib-0.4.27-cp310-cp310-win_amd64.whl new file mode 100644 index 000000000..fba8f932a Binary files /dev/null and b/build_helpers/TA_Lib-0.4.27-cp310-cp310-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.27-cp311-cp311-win_amd64.whl b/build_helpers/TA_Lib-0.4.27-cp311-cp311-win_amd64.whl new file mode 100644 index 000000000..63e6eacce Binary files /dev/null and b/build_helpers/TA_Lib-0.4.27-cp311-cp311-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.27-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.27-cp38-cp38-win_amd64.whl new file mode 100644 index 000000000..80a81af98 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.27-cp38-cp38-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.27-cp39-cp39-win_amd64.whl b/build_helpers/TA_Lib-0.4.27-cp39-cp39-win_amd64.whl new file mode 100644 index 000000000..7b3fb91f9 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.27-cp39-cp39-win_amd64.whl differ diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 2fc21d317..73a6eea06 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -1,21 +1,11 @@ -# Downloads don't work automatically, since the URL is regenerated via javascript. -# Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib +# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040 python -m pip install --upgrade pip wheel $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" -if ($pyv -eq '3.8') { - pip install build_helpers\TA_Lib-0.4.26-cp38-cp38-win_amd64.whl -} -if ($pyv -eq '3.9') { - pip install build_helpers\TA_Lib-0.4.26-cp39-cp39-win_amd64.whl -} -if ($pyv -eq '3.10') { - pip install build_helpers\TA_Lib-0.4.26-cp310-cp310-win_amd64.whl -} -if ($pyv -eq '3.11') { - pip install build_helpers\TA_Lib-0.4.26-cp311-cp311-win_amd64.whl -} + +pip install --find-links=build_helpers\ TA-Lib + pip install -r requirements-dev.txt pip install -e . diff --git a/build_helpers/pyarrow-12.0.0-cp39-cp39-linux_armv7l.whl b/build_helpers/pyarrow-12.0.1-cp39-cp39-linux_armv7l.whl similarity index 65% rename from build_helpers/pyarrow-12.0.0-cp39-cp39-linux_armv7l.whl rename to build_helpers/pyarrow-12.0.1-cp39-cp39-linux_armv7l.whl index 2a8d1ff51..55211ca01 100644 Binary files a/build_helpers/pyarrow-12.0.0-cp39-cp39-linux_armv7l.whl and b/build_helpers/pyarrow-12.0.1-cp39-cp39-linux_armv7l.whl differ diff --git a/docker/docker-compose-freqai.yml b/docker/docker-compose-freqai.yml index 6edf41238..ce493d5b7 100644 --- a/docker/docker-compose-freqai.yml +++ b/docker/docker-compose-freqai.yml @@ -32,5 +32,5 @@ services: --logfile /freqtrade/user_data/logs/freqtrade.log --db-url sqlite:////freqtrade/user_data/tradesv3.sqlite --config /freqtrade/user_data/config.json - --freqai-model XGBoostClassifier - --strategy SampleStrategy + --freqaimodel XGBoostRegressor + --strategy FreqaiExampleStrategy diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index b587c4157..3926fb5b1 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -103,6 +103,22 @@ The indicators have to be present in your strategy's main DataFrame (either for timeframe or for informative timeframes) otherwise they will simply be ignored in the script output. +There are a range of candle and trade-related fields that are included in the analysis so are +automatically accessible by including them on the indicator-list, and these include: + +- **open_date :** trade open datetime +- **close_date :** trade close datetime +- **min_rate :** minimum price seen throughout the position +- **max_rate :** maxiumum price seen throughout the position +- **open :** signal candle open price +- **close :** signal candle close price +- **high :** signal candle high price +- **low :** signal candle low price +- **volume :** signal candle volumne +- **profit_ratio :** trade profit ratio +- **profit_abs :** absolute profit return of the trade + + ### Filtering the trade output by date To show only trades between dates within your backtested timerange, supply the usual `timerange` option in `YYYYMMDD-[YYYYMMDD]` format: diff --git a/docs/backtesting.md b/docs/backtesting.md index 166c2b28b..d1cd61057 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -305,7 +305,7 @@ A backtesting result will look like that: | Sharpe | 2.97 | | Calmar | 6.29 | | Profit factor | 1.11 | -| Expectancy | -0.15 | +| Expectancy (Ratio) | -0.15 (-0.05) | | Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | @@ -324,6 +324,7 @@ A backtesting result will look like that: | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Max Consecutive Wins / Loss | 3 / 4 | | Rejected Entry signals | 3089 | | Entry/Exit Timeouts | 0 / 0 | | Canceled Trade Entries | 34 | @@ -409,7 +410,7 @@ It contains some useful key metrics about performance of your strategy on backte | Sharpe | 2.97 | | Calmar | 6.29 | | Profit factor | 1.11 | -| Expectancy | -0.15 | +| Expectancy (Ratio) | -0.15 (-0.05) | | Avg. stake amount | 0.001 BTC | | Total trade volume | 0.429 BTC | | | | @@ -428,6 +429,7 @@ It contains some useful key metrics about performance of your strategy on backte | Days win/draw/lose | 12 / 82 / 25 | | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | +| Max Consecutive Wins / Loss | 3 / 4 | | Rejected Entry signals | 3089 | | Entry/Exit Timeouts | 0 / 0 | | Canceled Trade Entries | 34 | @@ -467,6 +469,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. +- `Max Consecutive Wins / Loss`: Maximum consecutive wins/losses in a row. - `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached. - `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used). - `Canceled Trade Entries`: Number of trades that have been canceled by user request via `adjust_entry_price`. @@ -534,6 +537,7 @@ Since backtesting lacks some detailed information about what happens within a ca - 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 + - 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 - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` exit reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes diff --git a/docs/exchanges.md b/docs/exchanges.md index 997d012e1..fb3049ba5 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -259,10 +259,17 @@ The configuration parameter `exchange.unknown_fee_rate` can be used to specify t Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures. -On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors. +On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well. +API Keys for live futures trading (Subaccount on non-unified) must have the following permissions: +* Read-write +* Contract - Orders +* Contract - Positions + +We do strongly recommend to limit all API keys to the IP you're going to use it from. + !!! Tip "Stoploss on Exchange" Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use. diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index daf645339..a781d0834 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -261,7 +261,7 @@ class MyFreqaiModel(BaseRegressionModel): """ feature_pipeline = Pipeline([ ('qt', SKLearnWrapper(QuantileTransformer(output_distribution='normal'))), - ('di', ds.DissimilarityIndex(di_threshold=1) + ('di', ds.DissimilarityIndex(di_threshold=1)) ]) return feature_pipeline diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index cc92c2457..5e60d2a07 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -42,7 +42,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `use_SVM_to_remove_outliers` | Train a support vector machine to detect and remove outliers from the training dataset, as well as from incoming data points. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm).
**Datatype:** Boolean. | `svm_params` | All parameters available in Sklearn's `SGDOneClassSVM()`. See details about some select parameters [here](freqai-feature-engineering.md#identifying-outliers-using-a-support-vector-machine-svm).
**Datatype:** Dictionary. | `use_DBSCAN_to_remove_outliers` | Cluster data using the DBSCAN algorithm to identify and remove outliers from training and prediction data. See details about how it works [here](freqai-feature-engineering.md#identifying-outliers-with-dbscan).
**Datatype:** Boolean. -| `inlier_metric_window` | If set, FreqAI adds an `inlier_metric` to the training feature set and set the lookback to be the `inlier_metric_window`, i.e., the number of previous time points to compare the current candle to. Details of how the `inlier_metric` is computed can be found [here](freqai-feature-engineering.md#inlier-metric).
**Datatype:** Integer.
Default: `0`. | `noise_standard_deviation` | If set, FreqAI adds noise to the training features with the aim of preventing overfitting. FreqAI generates random deviates from a gaussian distribution with a standard deviation of `noise_standard_deviation` and adds them to all data points. `noise_standard_deviation` should be kept relative to the normalized space, i.e., between -1 and 1. In other words, since data in FreqAI is always normalized to be between -1 and 1, `noise_standard_deviation: 0.05` would result in 32% of the data being randomly increased/decreased by more than 2.5% (i.e., the percent of data falling within the first standard deviation).
**Datatype:** Integer.
Default: `0`. | `outlier_protection_percentage` | Enable to prevent outlier detection methods from discarding too much data. If more than `outlier_protection_percentage` % of points are detected as outliers by the SVM or DBSCAN, FreqAI will log a warning message and ignore outlier detection, i.e., the original dataset will be kept intact. If the outlier protection is triggered, no predictions will be made based on the training dataset.
**Datatype:** Float.
Default: `30`. | `reverse_train_test_order` | Split the feature dataset (see below) and use the latest data split for training and test on historical split of the data. This allows the model to be trained up to the most recent data point, while avoiding overfitting. However, you should be careful to understand the unorthodox nature of this parameter before employing it.
**Datatype:** Boolean.
Default: `False` (no reversal). diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 1babeca1f..9cdcc9bca 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -211,7 +211,7 @@ The user is responsible for providing a server or local file that returns a JSON ```json { "pairs": ["XRP/USDT", "ETH/USDT", "LTC/USDT"], - "refresh_period": 1800, + "refresh_period": 1800 } ``` diff --git a/docs/includes/showcase.md b/docs/includes/showcase.md new file mode 100644 index 000000000..297685ad4 --- /dev/null +++ b/docs/includes/showcase.md @@ -0,0 +1,11 @@ +This section will highlight a few projects from members of the community. +!!! Note + The projects below are for the most part not maintained by the freqtrade , therefore use your own caution before using them. + +- [Example freqtrade strategies](https://github.com/freqtrade/freqtrade-strategies/) +- [FrequentHippo - Grafana dashboard with dry/live runs and backtests](http://frequenthippo.ddns.net:3000/) (by hippocritical). +- [Online pairlist generator](https://remotepairlist.com/) (by Blood4rc). +- [Freqtrade Backtesting Project](https://bt.robot.co.network/) (by Blood4rc). +- [Freqtrade analysis notebook](https://github.com/froggleston/freqtrade_analysis_notebook) (by Froggleston). +- [TUI for freqtrade](https://github.com/froggleston/freqtrade-frogtrade9000) (by Froggleston). +- [Bot Academy](https://botacademy.ddns.net/) (by stash86) - Blog about crypto bot projects. diff --git a/docs/index.md b/docs/index.md index c24d1f36b..77542ae78 100644 --- a/docs/index.md +++ b/docs/index.md @@ -63,6 +63,10 @@ Exchanges confirmed working by the community: - [X] [Bitvavo](https://bitvavo.com/) - [X] [Kucoin](https://www.kucoin.com/) +## Community showcase + +--8<-- "includes/showcase.md" + ## Requirements ### Hardware requirements diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 9a784ba01..ab16dc0cf 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.3 -mkdocs-material==9.1.18 +mkdocs-material==9.1.19 mdx_truly_sane_lists==1.3 -pymdown-extensions==10.0.1 +pymdown-extensions==10.1 jinja2==3.1.2 diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 855f2353b..ab8eb9f98 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -750,7 +750,7 @@ class DigDeeperStrategy(IStrategy): # Hope you have a deep wallet! try: # This returns first order stake size - stake_amount = filled_entries[0].cost + stake_amount = filled_entries[0].stake_amount # This then calculates current safety order size stake_amount = stake_amount * (1 + (count_of_entries * 0.25)) return stake_amount diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 1b36c60ad..f501d0e49 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -287,12 +287,17 @@ Return a summary of your profit/loss and performance. > **Best Performing:** `PAY/BTC: 50.23%` > **Trading volume:** `0.5 BTC` > **Profit factor:** `1.04` +> **Win / Loss:** `102 / 36` +> **Winrate:** `73.91%` +> **Expectancy (Ratio):** `4.87 (1.66)` > **Max Drawdown:** `9.23% (0.01255 BTC)` The relative profit of `1.2%` is the average profit per trade. The relative profit of `15.2 Σ%` is be based on the starting capital - so in this case, the starting capital was `0.00485701 * 1.152 = 0.00738 BTC`. Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. +Expectancy corresponds to the average return per currency unit at risk, i.e. the winrate and the risk-reward ratio (the average gain of winning trades compared to the average loss of losing trades). +Expectancy Ratio is expected profit or loss of a subsequent trade based on the performance of all past trades. Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date. diff --git a/docs/trade-object.md b/docs/trade-object.md index 7e0db1e3b..15a8b1938 100644 --- a/docs/trade-object.md +++ b/docs/trade-object.md @@ -141,7 +141,8 @@ 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 +`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.*) +`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) `order_fill_date` | datetime | Order fill date **use `order_fill_utc` instead** diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 00c369919..e18a05e9b 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -80,12 +80,18 @@ When using the Form-Encoded or JSON-Encoded configuration you can configure any The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header. -Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries: +## Additional configurations + +The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. +You can also specify `webhook.timeout` - which defines how long the bot will wait until it assumes the other host as unresponsive (defaults to 10s). + +Example configuration for retries: ```json "webhook": { "enabled": true, "url": "https://", + "timeout": 10, "retries": 3, "retry_delay": 0.2, "status": { @@ -109,6 +115,8 @@ Custom messages can be sent to Webhook endpoints via the `self.dp.send_msg()` fu Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. +## Webhook Message types + ### Entry The fields in `webhook.entry` are filled when the bot executes a long/short. Parameters are filled using string.format. diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2c8dde56d..42ff08414 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2023.7.dev' +__version__ = '2023.8.dev' if 'dev' in __version__: from pathlib import Path diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 63bb5c211..311622458 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List from questionary import Separator, prompt +from freqtrade.configuration.detect_environment import running_in_docker from freqtrade.configuration.directory_operations import chown_user_directory from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import OperationalException @@ -179,7 +180,7 @@ def ask_user_config() -> Dict[str, Any]: "name": "api_server_listen_addr", "message": ("Insert Api server Listen Address (0.0.0.0 for docker, " "otherwise best left untouched)"), - "default": "127.0.0.1", + "default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", "when": lambda x: x['api_server'] }, { diff --git a/freqtrade/configuration/detect_environment.py b/freqtrade/configuration/detect_environment.py new file mode 100644 index 000000000..99d585e87 --- /dev/null +++ b/freqtrade/configuration/detect_environment.py @@ -0,0 +1,8 @@ +import os + + +def running_in_docker() -> bool: + """ + Check if we are running in a docker container + """ + return os.environ.get('FT_APP_ENV') == 'docker' diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index e1313749b..267a74928 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -3,6 +3,7 @@ import shutil from pathlib import Path from typing import Optional +from freqtrade.configuration.detect_environment import running_in_docker from freqtrade.constants import (USER_DATA_FILES, USERPATH_FREQAIMODELS, USERPATH_HYPEROPTS, USERPATH_NOTEBOOKS, USERPATH_STRATEGIES, Config) from freqtrade.exceptions import OperationalException @@ -30,8 +31,7 @@ def chown_user_directory(directory: Path) -> None: Use Sudo to change permissions of the home-directory if necessary Only applies when running in docker! """ - import os - if os.environ.get('FT_APP_ENV') == 'docker': + if running_in_docker(): try: import subprocess subprocess.check_output( diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index c5905acde..12be3c448 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -170,6 +170,7 @@ def load_and_merge_backtest_result(strategy_name: str, filename: Path, results: def _get_backtest_files(dirname: Path) -> List[Path]: + # Weird glob expression here avoids including .meta.json files. return list(reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json')))) @@ -184,7 +185,7 @@ def get_backtest_resultlist(dirname: Path): continue for s, v in metadata.items(): results.append({ - 'filename': filename.name, + 'filename': filename.stem, 'strategy': s, 'run_id': v['run_id'], 'backtest_start_time': v['backtest_start_time'], @@ -193,6 +194,17 @@ def get_backtest_resultlist(dirname: Path): return results +def delete_backtest_result(file_abs: Path): + """ + Delete backtest result file and corresponding metadata file. + """ + # *.meta.json + logger.info(f"Deleting backtest result file: {file_abs.name}") + file_abs_meta = file_abs.with_suffix('.meta.json') + file_abs.unlink() + file_abs_meta.unlink() + + def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, str], min_backtest_date: Optional[datetime] = None) -> Dict[str, Any]: """ @@ -211,7 +223,6 @@ def find_existing_backtest_stats(dirname: Union[Path, str], run_ids: Dict[str, s 'strategy_comparison': [], } - # Weird glob expression here avoids including .meta.json files. for filename in _get_backtest_files(dirname): metadata = load_backtest_metadata(filename) if not metadata: diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 4b4c89d08..0eb6faf77 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -96,8 +96,14 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) 'volume': 'sum' } timeframe_minutes = timeframe_to_minutes(timeframe) + resample_interval = f'{timeframe_minutes}min' + if timeframe_minutes >= 43200 and timeframe_minutes < 525600: + # Monthly candles need special treatment to stick to the 1st of the month + resample_interval = f'{timeframe}S' + elif timeframe_minutes > 43200: + resample_interval = timeframe # Resample to create "NAN" values - df = dataframe.resample(f'{timeframe_minutes}min', on='date').agg(ohlcv_dict) + df = dataframe.resample(resample_interval, on='date').agg(ohlcv_dict) # Forwardfill close for missing columns df['close'] = df['close'].fillna(method='ffill') @@ -122,7 +128,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) return df -def trim_dataframe(df: DataFrame, timerange, df_date_col: str = 'date', +def trim_dataframe(df: DataFrame, timerange, *, df_date_col: str = 'date', startup_candles: int = 0) -> DataFrame: """ Trim dataframe based on given timerange diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 138903b57..c22dcccef 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -194,32 +194,35 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1 -def calculate_expectancy(trades: pd.DataFrame) -> float: +def calculate_expectancy(trades: pd.DataFrame) -> Tuple[float, float]: """ Calculate expectancy :param trades: DataFrame containing trades (requires columns close_date and profit_abs) - :return: expectancy + :return: expectancy, expectancy_ratio """ - if len(trades) == 0: - return 0 - expectancy = 1 + expectancy = 0 + expectancy_ratio = 100 - profit_sum = trades.loc[trades['profit_abs'] > 0, 'profit_abs'].sum() - loss_sum = abs(trades.loc[trades['profit_abs'] < 0, 'profit_abs'].sum()) - nb_win_trades = len(trades.loc[trades['profit_abs'] > 0]) - nb_loss_trades = len(trades.loc[trades['profit_abs'] < 0]) + if len(trades) > 0: + winning_trades = trades.loc[trades['profit_abs'] > 0] + losing_trades = trades.loc[trades['profit_abs'] < 0] + profit_sum = winning_trades['profit_abs'].sum() + loss_sum = abs(losing_trades['profit_abs'].sum()) + nb_win_trades = len(winning_trades) + nb_loss_trades = len(losing_trades) - if (nb_win_trades > 0) and (nb_loss_trades > 0): - average_win = profit_sum / nb_win_trades - average_loss = loss_sum / nb_loss_trades - risk_reward_ratio = average_win / average_loss - winrate = nb_win_trades / len(trades) - expectancy = ((1 + risk_reward_ratio) * winrate) - 1 - elif nb_win_trades == 0: - expectancy = 0 + average_win = (profit_sum / nb_win_trades) if nb_win_trades > 0 else 0 + average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0 + winrate = (nb_win_trades / len(trades)) + loserate = (nb_loss_trades / len(trades)) - return expectancy + expectancy = (winrate * average_win) - (loserate * average_loss) + if (average_loss > 0): + risk_reward_ratio = average_win / average_loss + expectancy_ratio = ((1 + risk_reward_ratio) * winrate) - 1 + + return expectancy, expectancy_ratio def calculate_sortino(trades: pd.DataFrame, min_date: datetime, max_date: datetime, diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 47623f107..d863be03b 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -172,13 +172,7 @@ class Edge: pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - df_analyzed = self.strategy.advise_exit( - dataframe=self.strategy.advise_entry( - dataframe=pair_data, - metadata={'pair': pair} - ), - metadata={'pair': pair} - )[headers].copy() + df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair})[headers].copy() trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8075d775a..e8be804cb 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -34,6 +34,7 @@ class Binance(Exchange): "tickers_have_price": False, "floor_leverage": True, "stop_price_type_field": "workingType", + "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'], "stop_price_type_value_mapping": { PriceType.LAST: "CONTRACT_PRICE", PriceType.MARK: "MARK_PRICE", diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index b211be701..f1b2c8322 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -1374,10 +1374,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 15.0, + "maxLeverage": 10.0, "info": { "bracket": "1", - "initialLeverage": "15", + "initialLeverage": "10", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -1390,10 +1390,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "8", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -1406,10 +1406,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -1452,13 +1452,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -4744,13 +4744,13 @@ "tier": 4.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 250000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "4", "initialLeverage": "25", - "notionalCap": "250000", + "notionalCap": "500000", "notionalFloor": "50000", "maintMarginRatio": "0.02", "cum": "542.5" @@ -4759,71 +4759,71 @@ { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 500000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalCap": "2000000", + "notionalFloor": "500000", "maintMarginRatio": "0.05", - "cum": "8042.5" + "cum": "15542.5" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "4000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.1", - "cum": "58042.5" + "cum": "115542.5" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.125", - "cum": "108042.5" + "cum": "215542.5" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 10000000.0, + "minNotional": 8000000.0, + "maxNotional": 15000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalCap": "15000000", + "notionalFloor": "8000000", "maintMarginRatio": "0.15", - "cum": "233042.5" + "cum": "415542.5" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 10000000.0, + "minNotional": 15000000.0, "maxNotional": 20000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, @@ -4831,9 +4831,9 @@ "bracket": "9", "initialLeverage": "2", "notionalCap": "20000000", - "notionalFloor": "10000000", + "notionalFloor": "15000000", "maintMarginRatio": "0.25", - "cum": "1233042.5" + "cum": "1915542.5" } }, { @@ -4849,7 +4849,7 @@ "notionalCap": "50000000", "notionalFloor": "20000000", "maintMarginRatio": "0.5", - "cum": "6233042.5" + "cum": "6915542.5" } } ], @@ -5959,7 +5959,7 @@ } } ], - "BTC/USDT:USDT-230630": [ + "BTC/USDT:USDT-230929": [ { "tier": 1.0, "currency": "USDT", @@ -6060,13 +6060,13 @@ "tier": 7.0, "currency": "USDT", "minNotional": 40000000.0, - "maxNotional": 400000000.0, + "maxNotional": 120000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "400000000", + "notionalCap": "120000000", "notionalFloor": "40000000", "maintMarginRatio": "0.5", "cum": "1.246125E7" @@ -7382,13 +7382,13 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, + "maxNotional": 50000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "2", "initialLeverage": "20", - "notionalCap": "25000", + "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.025", "cum": "75.0" @@ -7397,39 +7397,39 @@ { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, + "minNotional": 50000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "200000", + "notionalFloor": "50000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "1325.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "11325.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, @@ -7437,9 +7437,9 @@ "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23825.0" } }, { @@ -7455,7 +7455,7 @@ "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "398825.0" } } ], @@ -8869,14 +8869,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.006, - "maxLeverage": 50.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", + "initialLeverage": "75", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.006", + "maintMarginRatio": "0.005", "cum": "0.0" } }, @@ -8884,128 +8884,144 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.007, - "maxLeverage": 40.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "40", - "notionalCap": "25000", + "initialLeverage": "50", + "notionalCap": "10000", "notionalFloor": "5000", - "maintMarginRatio": "0.007", + "maintMarginRatio": "0.006", "cum": "5.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 600000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.007, + "maxLeverage": 40.0, "info": { "bracket": "3", - "initialLeverage": "25", - "notionalCap": "600000", - "notionalFloor": "25000", - "maintMarginRatio": "0.01", - "cum": "80.0" + "initialLeverage": "40", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.007", + "cum": "15.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 600000.0, - "maxNotional": 900000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "minNotional": 50000.0, + "maxNotional": 750000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "4", - "initialLeverage": "20", - "notionalCap": "900000", - "notionalFloor": "600000", - "maintMarginRatio": "0.025", - "cum": "9080.0" + "initialLeverage": "25", + "notionalCap": "750000", + "notionalFloor": "50000", + "maintMarginRatio": "0.01", + "cum": "165.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 900000.0, - "maxNotional": 1800000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 750000.0, + "maxNotional": 1100000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "5", - "initialLeverage": "10", - "notionalCap": "1800000", - "notionalFloor": "900000", - "maintMarginRatio": "0.05", - "cum": "31580.0" + "initialLeverage": "20", + "notionalCap": "1100000", + "notionalFloor": "750000", + "maintMarginRatio": "0.025", + "cum": "11415.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1800000.0, - "maxNotional": 4800000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 1100000.0, + "maxNotional": 2200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "6", - "initialLeverage": "5", - "notionalCap": "4800000", - "notionalFloor": "1800000", - "maintMarginRatio": "0.1", - "cum": "121580.0" + "initialLeverage": "10", + "notionalCap": "2200000", + "notionalFloor": "1100000", + "maintMarginRatio": "0.05", + "cum": "38915.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 4800000.0, - "maxNotional": 6000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 2200000.0, + "maxNotional": 5600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "7", - "initialLeverage": "4", - "notionalCap": "6000000", - "notionalFloor": "4800000", - "maintMarginRatio": "0.125", - "cum": "241580.0" + "initialLeverage": "5", + "notionalCap": "5600000", + "notionalFloor": "2200000", + "maintMarginRatio": "0.1", + "cum": "148915.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 6000000.0, - "maxNotional": 18000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 5600000.0, + "maxNotional": 7000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "8", - "initialLeverage": "2", - "notionalCap": "18000000", - "notionalFloor": "6000000", - "maintMarginRatio": "0.25", - "cum": "991580.0" + "initialLeverage": "4", + "notionalCap": "7000000", + "notionalFloor": "5600000", + "maintMarginRatio": "0.125", + "cum": "288915.0" } }, { "tier": 9.0, "currency": "USDT", + "minNotional": 7000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "7000000", + "maintMarginRatio": "0.25", + "cum": "1163915.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", "minNotional": 18000000.0, "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "9", + "bracket": "10", "initialLeverage": "1", "notionalCap": "30000000", "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "5491580.0" + "cum": "5663915.0" } } ], @@ -10877,7 +10893,7 @@ } } ], - "ETH/USDT:USDT-230630": [ + "ETH/USDT:USDT-230929": [ { "tier": 1.0, "currency": "USDT", @@ -10978,13 +10994,13 @@ "tier": 7.0, "currency": "USDT", "minNotional": 40000000.0, - "maxNotional": 400000000.0, + "maxNotional": 120000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "400000000", + "notionalCap": "120000000", "notionalFloor": "40000000", "maintMarginRatio": "0.5", "cum": "1.246125E7" @@ -17721,6 +17737,234 @@ } } ], + "MAV/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], + "MDT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "MINA/USDT:USDT": [ { "tier": 1.0, @@ -18519,6 +18763,120 @@ } } ], + "NMR/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "OCEAN/USDT:USDT": [ { "tier": 1.0, @@ -22389,14 +22747,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 21.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "21", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -22413,7 +22771,7 @@ "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { @@ -22429,7 +22787,7 @@ "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { @@ -22445,7 +22803,7 @@ "notionalCap": "800000", "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "15700.0" + "cum": "15650.0" } }, { @@ -22461,7 +22819,7 @@ "notionalCap": "1000000", "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "35700.0" + "cum": "35650.0" } }, { @@ -22477,23 +22835,23 @@ "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "160700.0" + "cum": "160650.0" } }, { "tier": 7.0, "currency": "USDT", "minNotional": 3000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3500000", "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "910700.0" + "cum": "910650.0" } } ], @@ -22534,13 +22892,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 600000.0, + "maxNotional": 720000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "600000", + "notionalCap": "720000", "notionalFloor": "50000", "maintMarginRatio": "0.05", "cum": "1325.0" @@ -22549,39 +22907,39 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 600000.0, - "maxNotional": 1600000.0, + "minNotional": 720000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "1600000", - "notionalFloor": "600000", + "notionalCap": "2000000", + "notionalFloor": "720000", "maintMarginRatio": "0.1", - "cum": "31325.0" + "cum": "37325.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1600000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 2400000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "5", "initialLeverage": "4", - "notionalCap": "2000000", - "notionalFloor": "1600000", + "notionalCap": "2400000", + "notionalFloor": "2000000", "maintMarginRatio": "0.125", - "cum": "71325.0" + "cum": "87325.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, + "minNotional": 2400000.0, "maxNotional": 6000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, @@ -22589,9 +22947,9 @@ "bracket": "6", "initialLeverage": "2", "notionalCap": "6000000", - "notionalFloor": "2000000", + "notionalFloor": "2400000", "maintMarginRatio": "0.25", - "cum": "321325.0" + "cum": "387325.0" } }, { @@ -22607,7 +22965,7 @@ "notionalCap": "10000000", "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "1821325.0" + "cum": "1887325.0" } } ], @@ -22648,13 +23006,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 450000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "450000", + "notionalCap": "600000", "notionalFloor": "50000", "maintMarginRatio": "0.025", "cum": "300.0" @@ -22663,55 +23021,55 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 450000.0, - "maxNotional": 900000.0, + "minNotional": 600000.0, + "maxNotional": 1200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "900000", - "notionalFloor": "450000", + "notionalCap": "1200000", + "notionalFloor": "600000", "maintMarginRatio": "0.05", - "cum": "11550.0" + "cum": "15300.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 900000.0, - "maxNotional": 2400000.0, + "minNotional": 1200000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "2400000", - "notionalFloor": "900000", + "notionalCap": "3000000", + "notionalFloor": "1200000", "maintMarginRatio": "0.1", - "cum": "56550.0" + "cum": "75300.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2400000.0, - "maxNotional": 3000000.0, + "minNotional": 3000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "3000000", - "notionalFloor": "2400000", + "notionalCap": "4000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.125", - "cum": "116550.0" + "cum": "150300.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 3000000.0, + "minNotional": 4000000.0, "maxNotional": 6000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, @@ -22719,9 +23077,9 @@ "bracket": "7", "initialLeverage": "2", "notionalCap": "6000000", - "notionalFloor": "3000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.25", - "cum": "491550.0" + "cum": "650300.0" } }, { @@ -22737,7 +23095,7 @@ "notionalCap": "10000000", "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "1991550.0" + "cum": "2150300.0" } } ], @@ -24830,13 +25188,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 300000.0, + "maxNotional": 450000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "300000", + "notionalCap": "450000", "notionalFloor": "50000", "maintMarginRatio": "0.05", "cum": "1275.0" @@ -24845,39 +25203,39 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 300000.0, - "maxNotional": 800000.0, + "minNotional": 450000.0, + "maxNotional": 1200000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "800000", - "notionalFloor": "300000", + "notionalCap": "1200000", + "notionalFloor": "450000", "maintMarginRatio": "0.1", - "cum": "16275.0" + "cum": "23775.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 800000.0, - "maxNotional": 1000000.0, + "minNotional": 1200000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "5", "initialLeverage": "4", - "notionalCap": "1000000", - "notionalFloor": "800000", + "notionalCap": "1500000", + "notionalFloor": "1200000", "maintMarginRatio": "0.125", - "cum": "36275.0" + "cum": "53775.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, + "minNotional": 1500000.0, "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, @@ -24885,9 +25243,9 @@ "bracket": "6", "initialLeverage": "2", "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.25", - "cum": "161275.0" + "cum": "241275.0" } }, { @@ -24903,7 +25261,7 @@ "notionalCap": "5000000", "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "911275.0" + "cum": "991275.0" } } ], @@ -25721,6 +26079,104 @@ } } ], + "XVG/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "0", + "maintMarginRatio": "0.025", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "2", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "625.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "3", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10625.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "4", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23125.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "5", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148125.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "6", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898125.0" + } + } + ], "XVS/USDT:USDT": [ { "tier": 1.0, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2cf98c266..5f2530431 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -80,9 +80,8 @@ class Exchange: "mark_ohlcv_price": "mark", "mark_ohlcv_timeframe": "8h", "ccxt_futures_name": "swap", - "fee_cost_in_contracts": False, # Fee cost needs contract conversion "needs_trading_fees": False, # use fetch_trading_fees to cache fees - "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'], + "order_props_in_contracts": ['amount', 'filled', 'remaining'], # Override createMarketBuyOrderRequiresPrice where ccxt has it wrong "marketOrderRequiresPrice": False, } @@ -1859,9 +1858,6 @@ class Exchange: if fee_curr is None: return None fee_cost = float(fee['cost']) - if self._ft_has['fee_cost_in_contracts']: - # Convert cost via "contracts" conversion - fee_cost = self._contracts_to_amount(symbol, fee['cost']) # Calculate fee based on order details if fee_curr == self.get_pair_base_currency(symbol): diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index eceab4ec1..d36f57da7 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -33,8 +33,6 @@ class Gate(Exchange): _ft_has_futures: Dict = { "needs_trading_fees": True, "marketOrderRequiresPrice": False, - "fee_cost_in_contracts": False, # Set explicitly to false for clarity - "order_props_in_contracts": ['amount', 'filled', 'remaining'], "stop_price_type_field": "price_type", "stop_price_type_value_mapping": { PriceType.LAST: 0, diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index c703e3a78..7b1c90515 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -32,7 +32,6 @@ class Okx(Exchange): } _ft_has_futures: Dict = { "tickers_have_quoteVolume": False, - "fee_cost_in_contracts": True, "stop_price_type_field": "slTriggerPxType", "stop_price_type_value_mapping": { PriceType.LAST: "last", diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index 36c94130c..efae6d060 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -86,8 +86,6 @@ class IFreqaiModel(ABC): logger.warning("DI threshold is not configured for Keras models yet. Deactivating.") self.CONV_WIDTH = self.freqai_info.get('conv_width', 1) - if self.ft_params.get("inlier_metric_window", 0): - self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 self.class_names: List[str] = [] # used in classification subclasses self.pair_it = 0 self.pair_it_train = 0 @@ -676,15 +674,6 @@ class IFreqaiModel(ABC): hist_preds_df['close_price'] = strat_df['close'] hist_preds_df['date_pred'] = strat_df['date'] - # # for keras type models, the conv_window needs to be prepended so - # # viewing is correct in frequi - if self.ft_params.get('inlier_metric_window', 0): - n_lost_points = self.freqai_info.get('conv_width', 2) - zeros_df = DataFrame(np.zeros((n_lost_points, len(hist_preds_df.columns))), - columns=hist_preds_df.columns) - self.dd.historic_predictions[pair] = pd.concat( - [zeros_df, hist_preds_df], axis=0, ignore_index=True) - def fit_live_predictions(self, dk: FreqaiDataKitchen, pair: str) -> None: """ Fit the labels with a gaussian distribution diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index 45f3a31d0..4c481adff 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -32,8 +32,8 @@ class LightGBMClassifier(BaseClassifierModel): eval_set = None test_weights = None else: - eval_set = (data_dictionary["test_features"].to_numpy(), - data_dictionary["test_labels"].to_numpy()[:, 0]) + eval_set = [(data_dictionary["test_features"].to_numpy(), + data_dictionary["test_labels"].to_numpy()[:, 0])] test_weights = data_dictionary["test_weights"] X = data_dictionary["train_features"].to_numpy() y = data_dictionary["train_labels"].to_numpy()[:, 0] @@ -42,7 +42,6 @@ class LightGBMClassifier(BaseClassifierModel): init_model = self.get_init_model(dk.pair) model = LGBMClassifier(**self.model_training_parameters) - model.fit(X=X, y=y, eval_set=eval_set, sample_weight=train_weights, eval_sample_weight=[test_weights], init_model=init_model) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 3d1c30ed3..15849f446 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -32,7 +32,7 @@ class LightGBMRegressor(BaseRegressionModel): eval_set = None eval_weights = None else: - eval_set = (data_dictionary["test_features"], data_dictionary["test_labels"]) + eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] eval_weights = data_dictionary["test_weights"] X = data_dictionary["train_features"] y = data_dictionary["train_labels"] diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 663a611f0..5827dcefe 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -42,10 +42,10 @@ class LightGBMRegressorMultiTarget(BaseRegressionModel): eval_weights = [data_dictionary["test_weights"]] eval_sets = [(None, None)] * data_dictionary['test_labels'].shape[1] # type: ignore for i in range(data_dictionary['test_labels'].shape[1]): - eval_sets[i] = ( # type: ignore + eval_sets[i] = [( # type: ignore data_dictionary["test_features"], data_dictionary["test_labels"].iloc[:, i] - ) + )] init_model = self.get_init_model(dk.pair) if init_model: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 562cdc2e0..9d35aee0f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1383,7 +1383,10 @@ class FreqtradeBot(LoggingMixin): latest_candle_close_date = timeframe_to_next_date(self.strategy.timeframe, latest_candle_open_date) # Check if new candle - if order_obj and latest_candle_close_date > order_obj.order_date_utc: + if ( + order_obj and order_obj.side == trade.entry_side + and latest_candle_close_date > order_obj.order_date_utc + ): # New candle proposed_rate = self.exchange.get_rate( trade.pair, side='entry', is_short=trade.is_short, refresh=True) @@ -1939,6 +1942,7 @@ class FreqtradeBot(LoggingMixin): """ Applies the fee to amount (either from Order or from Trades). Can eat into dust if more than the required asset is available. + In case of trade adjustment orders, trade.amount will not have been adjusted yet. Can't happen in Futures mode - where Fees are always in settlement currency, never in base currency. """ @@ -1948,6 +1952,10 @@ class FreqtradeBot(LoggingMixin): # check against remaining amount! 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. + amount_ = trade.amount + amount + if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount_: # Eat into dust if we own more than base currency logger.info(f"Fee amount for {trade} was in base currency - " @@ -1977,7 +1985,11 @@ class FreqtradeBot(LoggingMixin): # Init variables order_amount = safe_value_fallback(order, 'filled', 'amount') # Only run for closed orders - if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': + if ( + trade.fee_updated(order.get('side', '')) + or order['status'] == 'open' + or order_obj.ft_fee_base + ): return None trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 350ac5eef..e715c280a 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -116,6 +116,13 @@ def file_load_json(file: Path): return pairdata +def is_file_in_dir(file: Path, directory: Path) -> bool: + """ + Helper function to check if file is in directory. + """ + return file.is_file() and file.parent.samefile(directory) + + def pair_to_filename(pair: str) -> str: for ch in ['/', ' ', '.', '@', '$', '+', ':']: pair = pair.replace(ch, '_') diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a168b7eb6..d71614442 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -367,11 +367,7 @@ class Backtesting: if not pair_data.empty: # Cleanup from prior runs pair_data.drop(HEADERS[5:] + ['buy', 'sell'], axis=1, errors='ignore') - - df_analyzed = self.strategy.advise_exit( - self.strategy.advise_entry(pair_data, {'pair': pair}), - {'pair': pair} - ).copy() + df_analyzed = self.strategy.ft_advise_signals(pair_data, {'pair': pair}) # Trim startup period from analyzed dataframe df_analyzed = processed[pair] = pair_data = trim_dataframe( df_analyzed, self.timerange, startup_candles=self.required_startup) @@ -679,6 +675,7 @@ class Backtesting: remaining=amount, cost=amount * close_rate, ) + order._trade_bt = trade trade.orders.append(order) return trade @@ -901,8 +898,9 @@ class Backtesting: amount=amount, filled=0, remaining=amount, - cost=stake_amount + trade.fee_open, + cost=amount * propose_rate + trade.fee_open, ) + order._trade_bt = trade trade.orders.append(order) if pos_adjust and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_time, trade) @@ -1275,6 +1273,7 @@ class Backtesting: preprocessed = self.strategy.advise_all_indicators(data) # Trim startup period from analyzed dataframe + # This only used to determine if trimming would result in an empty dataframe preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup) if not preprocessed_tmp: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index fe590f0d2..cba38d84a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -446,6 +446,8 @@ class Hyperopt: preprocessed = self.backtesting.strategy.advise_all_indicators(data) # Trim startup period from analyzed dataframe to get correct dates for output. + # This is only used to keep track of min/max date after trimming. + # The result is NOT returned from this method, actual trimming happens in backtesting. trimmed = trim_dataframes(preprocessed, self.timerange, self.backtesting.required_startup) self.min_date, self.max_date = get_timerange(trimmed) if not self.market_change: diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index bc5b85309..6bbf3c15d 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -432,12 +432,10 @@ class HyperoptTools: for i in range(len(trials)): if trials.loc[i]['is_profit']: for j in range(len(trials.loc[i]) - 3): - trials.iat[i, j] = "{}{}{}".format(Fore.GREEN, - str(trials.loc[i][j]), Fore.RESET) + trials.iat[i, j] = f"{Fore.GREEN}{str(trials.loc[i][j])}{Fore.RESET}" if trials.loc[i]['is_best'] and highlight_best: for j in range(len(trials.loc[i]) - 3): - trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT, - str(trials.loc[i][j]), Style.RESET_ALL) + trials.iat[i, j] = f"{Style.BRIGHT}{str(trials.loc[i][j])}{Style.RESET_ALL}" trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random']) if remove_header > 0: diff --git a/freqtrade/optimize/optimize_reports/__init__.py b/freqtrade/optimize/optimize_reports/__init__.py index 68e222d00..9e3ac46bc 100644 --- a/freqtrade/optimize/optimize_reports/__init__.py +++ b/freqtrade/optimize/optimize_reports/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 from freqtrade.optimize.optimize_reports.bt_output import (generate_edge_table, + generate_wins_draws_losses, show_backtest_result, show_backtest_results, show_sorted_pairlist, @@ -14,5 +15,4 @@ 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_wins_draws_losses) + 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 1fd1f7a34..eb30d0c97 100644 --- a/freqtrade/optimize/optimize_reports/bt_output.py +++ b/freqtrade/optimize/optimize_reports/bt_output.py @@ -5,8 +5,7 @@ from tabulate import tabulate from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.misc import decimals_per_coin, round_coin_value -from freqtrade.optimize.optimize_reports.optimize_reports import (generate_periodic_breakdown_stats, - generate_wins_draws_losses) +from freqtrade.optimize.optimize_reports.optimize_reports import generate_periodic_breakdown_stats logger = logging.getLogger(__name__) @@ -30,6 +29,16 @@ def _get_line_header(first_column: str, stake_currency: str, 'Win Draw Loss Win%'] +def generate_wins_draws_losses(wins, draws, losses): + if wins > 0 and losses == 0: + wl_ratio = '100' + elif wins == 0: + wl_ratio = '0' + else: + wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100' + return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}' + + def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe @@ -233,8 +242,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Calmar', f"{strat_results['calmar']:.2f}" if 'calmar' in strat_results else 'N/A'), ('Profit factor', f'{strat_results["profit_factor"]:.2f}' if 'profit_factor' in strat_results else 'N/A'), - ('Expectancy', f"{strat_results['expectancy']:.2f}" if 'expectancy' - in strat_results else 'N/A'), + ('Expectancy (Ratio)', ( + f"{strat_results['expectancy']:.2f} ({strat_results['expectancy_ratio']:.2f})" if + 'expectancy_ratio' in strat_results else 'N/A')), ('Trades per day', strat_results['trades_per_day']), ('Avg. daily profit %', f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"), @@ -260,6 +270,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), + ('Max Consecutive Wins / Loss', + f"{strat_results['max_consecutive_wins']} / {strat_results['max_consecutive_losses']}" + if 'max_consecutive_losses' in strat_results else 'N/A'), ('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')), ('Entry/Exit Timeouts', f"{strat_results.get('timedout_entry_orders', 'N/A')} / " diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 015f163e3..f24e30318 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -1,9 +1,10 @@ import logging from copy import deepcopy from datetime import datetime, timedelta, timezone -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Tuple, Union -from pandas import DataFrame, concat, to_datetime +import numpy as np +from pandas import DataFrame, Series, concat, to_datetime from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, IntOrInf from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, @@ -57,16 +58,6 @@ def generate_rejected_signals(preprocessed_df: Dict[str, DataFrame], return rejected_candles_only -def generate_wins_draws_losses(wins, draws, losses): - if wins > 0 and losses == 0: - wl_ratio = '100' - elif wins == 0: - wl_ratio = '0' - else: - wl_ratio = f'{100.0 / (wins + draws + losses) * wins:.1f}' if losses > 0 else '100' - return f'{wins:>4} {draws:>4} {losses:>4} {wl_ratio:>4}' - - def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: """ Generate one result dict, with "first_column" as key. @@ -97,6 +88,7 @@ def _generate_result_line(result: DataFrame, starting_balance: int, first_column '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]) / len(result) if len(result) else 0.0, } @@ -184,6 +176,7 @@ def generate_exit_reason_stats(max_open_trades: IntOrInf, results: DataFrame) -> '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, @@ -238,6 +231,7 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic wins = sum(day['profit_abs'] > 0) draws = sum(day['profit_abs'] == 0) loses = sum(day['profit_abs'] < 0) + trades = (wins + draws + loses) stats.append( { 'date': name.strftime('%d/%m/%Y'), @@ -245,7 +239,8 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic 'profit_abs': profit_abs, 'wins': wins, 'draws': draws, - 'loses': loses + 'loses': loses, + 'winrate': wins / trades if trades else 0.0, } ) return stats @@ -258,6 +253,23 @@ def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]: return result +def calc_streak(dataframe: DataFrame) -> Tuple[int, int]: + """ + Calculate consecutive win and loss streaks + :param dataframe: Dataframe containing the trades dataframe, with profit_ratio column + :return: Tuple containing consecutive wins and losses + """ + + df = Series(np.where(dataframe['profit_ratio'] > 0, 'win', 'loss')).to_frame('result') + df['streaks'] = df['result'].ne(df['result'].shift()).cumsum().rename('streaks') + df['counter'] = df['streaks'].groupby(df['streaks']).cumcount() + 1 + res = df.groupby(df['result']).max() + # + cons_wins = int(res.loc['win', 'counter']) if 'win' in res.index else 0 + cons_losses = int(res.loc['loss', 'counter']) if 'loss' in res.index else 0 + return cons_wins, cons_losses + + def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: """ Generate overall trade statistics """ if len(results) == 0: @@ -265,9 +277,12 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: 'wins': 0, 'losses': 0, 'draws': 0, + 'winrate': 0, 'holding_avg': timedelta(), 'winner_holding_avg': timedelta(), 'loser_holding_avg': timedelta(), + 'max_consecutive_wins': 0, + 'max_consecutive_losses': 0, } winning_trades = results.loc[results['profit_ratio'] > 0] @@ -280,17 +295,21 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: if not winning_trades.empty else timedelta()) loser_holding_avg = (timedelta(minutes=round(losing_trades['trade_duration'].mean())) if not losing_trades.empty else timedelta()) + winstreak, loss_streak = calc_streak(results) return { 'wins': len(winning_trades), 'losses': len(losing_trades), 'draws': len(draw_trades), + 'winrate': len(winning_trades) / len(results) if len(results) else 0.0, 'holding_avg': holding_avg, 'holding_avg_s': holding_avg.total_seconds(), 'winner_holding_avg': winner_holding_avg, 'winner_holding_avg_s': winner_holding_avg.total_seconds(), 'loser_holding_avg': loser_holding_avg, 'loser_holding_avg_s': loser_holding_avg.total_seconds(), + 'max_consecutive_wins': winstreak, + 'max_consecutive_losses': loss_streak, } @@ -383,6 +402,7 @@ def generate_strategy_stats(pairlist: List[str], losing_profit = results.loc[results['profit_abs'] < 0, 'profit_abs'].sum() profit_factor = winning_profit / abs(losing_profit) if losing_profit else 0.0 + expectancy, expectancy_ratio = calculate_expectancy(results) backtest_days = (max_date - min_date).days or 1 strat_stats = { 'trades': results.to_dict(orient='records'), @@ -408,7 +428,8 @@ def generate_strategy_stats(pairlist: List[str], 'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(), 'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(), 'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']), - 'expectancy': calculate_expectancy(results), + 'expectancy': expectancy, + 'expectancy_ratio': expectancy_ratio, 'sortino': calculate_sortino(results, min_date, max_date, start_balance), 'sharpe': calculate_sharpe(results, min_date, max_date, start_balance), 'calmar': calculate_calmar(results, min_date, max_date, start_balance), diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 6e9c2970d..f686e4f8c 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -38,6 +38,7 @@ class Order(ModelBase): Mirrors CCXT Order structure """ __tablename__ = 'orders' + __allow_unmapped__ = True session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id @@ -47,7 +48,8 @@ class Order(ModelBase): id: Mapped[int] = mapped_column(Integer, primary_key=True) ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True) - trade: Mapped["Trade"] = relationship("Trade", back_populates="orders") + _trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders") + _trade_bt: "LocalTrade" = None # type: ignore # order_side can only be 'buy', 'sell' or 'stoploss' ft_order_side: Mapped[str] = mapped_column(String(25), nullable=False) @@ -119,6 +121,15 @@ class Order(ModelBase): def safe_amount_after_fee(self) -> float: return self.safe_filled - self.safe_fee_base + @property + def trade(self) -> "LocalTrade": + return self._trade_bt or self._trade_live + + @property + def stake_amount(self) -> float: + """ Amount in stake currency used for this order""" + return self.safe_amount * self.safe_price / self.trade.leverage + def __repr__(self): return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, " @@ -1299,9 +1310,12 @@ class Trade(ModelBase, LocalTrade): Float(), nullable=True, default=None) # type: ignore def __init__(self, **kwargs): + from_json = kwargs.pop('__FROM_JSON', None) super().__init__(**kwargs) - self.realized_profit = 0 - self.recalc_open_trade_value() + if not from_json: + # Skip recalculation when loading from json + self.realized_profit = 0 + self.recalc_open_trade_value() @validates('enter_tag', 'exit_reason') def validate_string_len(self, key, value): @@ -1655,6 +1669,7 @@ class Trade(ModelBase, LocalTrade): import rapidjson data = rapidjson.loads(json_str) trade = cls( + __FROM_JSON=True, id=data["trade_id"], pair=data["pair"], base_currency=data["base_currency"], diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 5d16e74ba..9ed7bbc46 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -84,7 +84,7 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): except ValueError as e: raise OperationalException(e) from e if not trades.empty: - trades = trim_dataframe(trades, timerange, 'open_date') + trades = trim_dataframe(trades, timerange, df_date_col='open_date') return {"ohlcv": data, "trades": trades, diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index 66b7d9496..2f03678e2 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -3,15 +3,16 @@ Remote PairList provider Provides pair list fetched from a remote source """ -import json import logging from pathlib import Path from typing import Any, Dict, List, Tuple +import rapidjson import requests from cachetools import TTLCache from freqtrade import __version__ +from freqtrade.configuration.load_config import CONFIG_PARSE_MODE from freqtrade.constants import Config from freqtrade.exceptions import OperationalException from freqtrade.exchange.types import Tickers @@ -236,7 +237,7 @@ class RemotePairList(IPairList): if file_path.exists(): with file_path.open() as json_file: # Load the JSON data into a dictionary - jsonparse = json.load(json_file) + jsonparse = rapidjson.load(json_file, parse_mode=CONFIG_PARSE_MODE) try: pairlist = self.process_json(jsonparse) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 411ba4978..9c17bc9e0 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -2,6 +2,7 @@ import asyncio import logging from copy import deepcopy from datetime import datetime +from pathlib import Path from typing import Any, Dict, List from fastapi import APIRouter, BackgroundTasks, Depends @@ -9,11 +10,12 @@ from fastapi.exceptions import HTTPException from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.constants import Config -from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result +from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_resultlist, + load_and_merge_backtest_result) from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange.common import remove_exchange_credentials -from freqtrade.misc import deep_merge_dicts +from freqtrade.misc import deep_merge_dicts, is_file_in_dir from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest, BacktestResponse) from freqtrade.rpc.api_server.deps import get_config @@ -245,13 +247,16 @@ def api_backtest_history(config=Depends(get_config)): tags=['webserver', 'backtest']) def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config)): # Get backtest result history, read from metadata files - fn = config['user_data_dir'] / 'backtest_results' / filename + bt_results_base: Path = config['user_data_dir'] / 'backtest_results' + fn = (bt_results_base / filename).with_suffix('.json') + results: Dict[str, Any] = { 'metadata': {}, 'strategy': {}, 'strategy_comparison': [], } - + if not is_file_in_dir(fn, bt_results_base): + raise HTTPException(status_code=404, detail="File not found.") load_and_merge_backtest_result(strategy, fn, results) return { "status": "ended", @@ -261,3 +266,17 @@ def api_backtest_history_result(filename: str, strategy: str, config=Depends(get "status_msg": "Historic result", "backtest_result": results, } + + +@router.delete('/backtest/history/{file}', response_model=List[BacktestHistoryEntry], + tags=['webserver', 'backtest']) +def api_delete_backtest_history_entry(file: str, config=Depends(get_config)): + # Get backtest result history, read from metadata files + bt_results_base: Path = config['user_data_dir'] / 'backtest_results' + file_abs = (bt_results_base / file).with_suffix('.json') + # 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.") + + delete_backtest_result(file_abs) + return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results') diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 2c2d40a3d..04769b119 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -136,6 +136,9 @@ class Profit(BaseModel): winning_trades: int losing_trades: int profit_factor: float + winrate: float + expectancy: float + expectancy_ratio: float max_drawdown: float max_drawdown_abs: float trading_volume: Optional[float] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 143f110f0..3e5d55f71 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -49,7 +49,8 @@ logger = logging.getLogger(__name__) # 2.28: Switch reload endpoint to Post # 2.29: Add /exchanges endpoint # 2.30: new /pairlists endpoint -API_VERSION = 2.30 +# 2.31: new /backtest/history/ delete endpoint +API_VERSION = 2.31 # Public API, requires no auth. router_public = APIRouter() @@ -268,7 +269,10 @@ def pair_history(pair: str, timeframe: str, timerange: str, strategy: str, 'timerange': timerange, 'freqaimodel': freqaimodel if freqaimodel else config.get('freqaimodel'), }) - return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange) + try: + return RPC._rpc_analysed_history_full(config, pair, timeframe, exchange) + except Exception as e: + raise HTTPException(status_code=502, detail=str(e)) @router.get('/plot_config', response_model=PlotConfig, tags=['candle data']) @@ -283,7 +287,10 @@ def plot_config(strategy: Optional[str] = None, config=Depends(get_config), config1.update({ 'strategy': strategy }) + try: return PlotConfig.parse_obj(RPC._rpc_plot_config_with_strategy(config1)) + except Exception as e: + raise HTTPException(status_code=502, detail=str(e)) @router.get('/strategies', response_model=StrategyListResponse, tags=['strategy']) @@ -308,7 +315,8 @@ def get_strategy(strategy: str, config=Depends(get_config)): extra_dir=config_.get('strategy_path')) except OperationalException: raise HTTPException(status_code=404, detail='Strategy not found') - + except Exception as e: + raise HTTPException(status_code=502, detail=str(e)) return { 'strategy': strategy_obj.get_strategy_name(), 'code': strategy_obj.__source__, diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py index e1a277b30..b701b4901 100644 --- a/freqtrade/rpc/api_server/web_ui.py +++ b/freqtrade/rpc/api_server/web_ui.py @@ -30,7 +30,7 @@ async def ui_version(): } -def is_relative_to(path, base) -> bool: +def is_relative_to(path: Path, base: Path) -> bool: # Helper function simulating behaviour of is_relative_to, which was only added in python 3.9 try: path.relative_to(base) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index 24c34af72..ea13209c4 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -25,6 +25,7 @@ coingecko_mapping = { 'bnb': 'binancecoin', 'sol': 'solana', 'usdt': 'tether', + 'busd': 'binance-usd', } diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 35af370e4..466c99aa1 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -18,7 +18,7 @@ from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT, Config from freqtrade.data.history import load_data -from freqtrade.data.metrics import calculate_max_drawdown +from freqtrade.data.metrics import calculate_expectancy, calculate_max_drawdown from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, SignalDirection, State, TradingMode) from freqtrade.exceptions import ExchangeError, PricingError @@ -494,6 +494,8 @@ class RPC: profit_all_coin.append(profit_abs) profit_all_ratio.append(profit_ratio) + closed_trade_count = len([t for t in trades if not t.is_open]) + best_pair = Trade.get_best_pair(start_date) trading_volume = Trade.get_trading_volume(start_date) @@ -521,9 +523,14 @@ class RPC: profit_factor = winning_profit / abs(losing_profit) if losing_profit else float('inf') + winrate = (winning_trades / closed_trade_count) if closed_trade_count > 0 else 0 + trades_df = DataFrame([{'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'profit_abs': trade.close_profit_abs} for trade in trades if not trade.is_open and trade.close_date]) + + expectancy, expectancy_ratio = calculate_expectancy(trades_df) + max_drawdown_abs = 0.0 max_drawdown = 0.0 if len(trades_df) > 0: @@ -562,7 +569,7 @@ class RPC: 'profit_all_percent': round(profit_all_ratio_fromstart * 100, 2), 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), - 'closed_trade_count': len([t for t in trades if not t.is_open]), + 'closed_trade_count': closed_trade_count, 'first_trade_date': first_date.strftime(DATETIME_PRINT_FORMAT) if first_date else '', 'first_trade_humanized': dt_humanize(first_date) if first_date else '', 'first_trade_timestamp': int(first_date.timestamp() * 1000) if first_date else 0, @@ -576,6 +583,9 @@ class RPC: 'winning_trades': winning_trades, 'losing_trades': losing_trades, 'profit_factor': profit_factor, + 'winrate': winrate, + 'expectancy': expectancy, + 'expectancy_ratio': expectancy_ratio, 'max_drawdown': max_drawdown, 'max_drawdown_abs': max_drawdown_abs, 'trading_volume': trading_volume, @@ -1169,8 +1179,8 @@ class RPC: """ Analyzed dataframe in Dict form """ _data, last_analyzed = self.__rpc_analysed_dataframe_raw(pair, timeframe, limit) - return self._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) def __rpc_analysed_dataframe_raw( self, @@ -1240,27 +1250,34 @@ class RPC: exchange) -> Dict[str, Any]: timerange_parsed = TimeRange.parse_timerange(config.get('timerange')) + from freqtrade.data.converter import trim_dataframe + from freqtrade.data.dataprovider import DataProvider + from freqtrade.resolvers.strategy_resolver import StrategyResolver + + strategy = StrategyResolver.load_strategy(config) + startup_candles = strategy.startup_candle_count + _data = load_data( datadir=config["datadir"], pairs=[pair], timeframe=timeframe, timerange=timerange_parsed, data_format=config['dataformat_ohlcv'], - candle_type=config.get('candle_type_def', CandleType.SPOT) + candle_type=config.get('candle_type_def', CandleType.SPOT), + startup_candles=startup_candles, ) if pair not in _data: raise RPCException( f"No data for {pair}, {timeframe} in {config.get('timerange')} found.") - from freqtrade.data.dataprovider import DataProvider - from freqtrade.resolvers.strategy_resolver import StrategyResolver - strategy = StrategyResolver.load_strategy(config) + strategy.dp = DataProvider(config, exchange=exchange, pairlists=None) strategy.ft_bot_start() df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) + 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, dt_now()) + df_analyzed.copy(), dt_now()) 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 aad7fd8c4..aced89d7a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -849,6 +849,10 @@ class Telegram(RPCHandler): avg_duration = stats['avg_duration'] best_pair = stats['best_pair'] best_pair_profit_ratio = stats['best_pair_profit_ratio'] + winrate = stats['winrate'] + expectancy = stats['expectancy'] + expectancy_ratio = stats['expectancy_ratio'] + if stats['trade_count'] == 0: markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" else: @@ -873,7 +877,9 @@ class Telegram(RPCHandler): f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " f"`{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}`\n" - f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" + f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`\n" + f"*Winrate:* `{winrate:.2%}`\n" + f"*Expectancy (Ratio):* `{expectancy:.2f} ({expectancy_ratio:.2f})`" ) if stats['closed_trade_count'] > 0: markdown_msg += ( diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 80690ec0c..b9bdbd435 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -34,6 +34,7 @@ class Webhook(RPCHandler): self._format = self._config['webhook'].get('format', 'form') self._retries = self._config['webhook'].get('retries', 0) self._retry_delay = self._config['webhook'].get('retry_delay', 0.1) + self._timeout = self._config['webhook'].get('timeout', 10) def cleanup(self) -> None: """ @@ -107,12 +108,13 @@ class Webhook(RPCHandler): try: if self._format == 'form': - response = post(self._url, data=payload) + response = post(self._url, data=payload, timeout=self._timeout) elif self._format == 'json': - response = post(self._url, json=payload) + response = post(self._url, json=payload, timeout=self._timeout) elif self._format == 'raw': response = post(self._url, data=payload['data'], - headers={'Content-Type': 'text/plain'}) + headers={'Content-Type': 'text/plain'}, + timeout=self._timeout) else: raise NotImplementedError(f'Unknown format: {self._format}') diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1e9ebe1ae..0f848130f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -825,6 +825,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ Parses the given candle (OHLCV) data and returns a populated DataFrame add several TA indicators and entry order signal to it + Should only be used in live. :param dataframe: Dataframe containing data from exchange :param metadata: Metadata dictionary with additional data (e.g. 'pair') :return: DataFrame of candle (OHLCV) data with indicator data and signals added @@ -1321,6 +1322,20 @@ class IStrategy(ABC, HyperStrategyMixin): return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}).copy() for pair, pair_data in data.items()} + def ft_advise_signals(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Call advise_entry and advise_exit and return the resulting dataframe. + :param dataframe: Dataframe containing data from exchange, as well as pre-calculated + indicators + :param metadata: Metadata dictionary with additional data (e.g. 'pair') + :return: DataFrame of candle (OHLCV) data with indicator data and signals added + + """ + + dataframe = self.advise_entry(dataframe, metadata) + dataframe = self.advise_exit(dataframe, metadata) + return dataframe + def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Populate indicators that will be used in the Buy, Sell, short, exit_short strategy diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index da64515a4..ceef8d158 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -84,6 +84,7 @@ class Wallets: tot_profit = Trade.get_total_closed_profit() else: tot_profit = LocalTrade.total_profit + tot_profit += sum(trade.realized_profit for trade in open_trades) tot_in_trades = sum(trade.stake_amount for trade in open_trades) used_stake = 0.0 diff --git a/requirements-dev.txt b/requirements-dev.txt index 1a1914ead..08d269063 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,11 +7,11 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.277 +ruff==0.0.280 mypy==1.4.1 pre-commit==3.3.3 pytest==7.4.0 -pytest-asyncio==0.21.0 +pytest-asyncio==0.21.1 pytest-cov==4.1.0 pytest-mock==3.11.1 pytest-random-order==1.1.0 @@ -20,11 +20,11 @@ isort==5.12.0 time-machine==2.11.0 # Convert jupyter notebooks to markdown documents -nbconvert==7.6.0 +nbconvert==7.7.2 # mypy types -types-cachetools==5.3.0.5 +types-cachetools==5.3.0.6 types-filelock==3.2.7 -types-requests==2.31.0.1 -types-tabulate==0.9.0.2 -types-python-dateutil==2.8.19.13 +types-requests==2.31.0.2 +types-tabulate==0.9.0.3 +types-python-dateutil==2.8.19.14 diff --git a/requirements-freqai.txt b/requirements-freqai.txt index ceb5488a6..325b92544 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -6,7 +6,7 @@ scikit-learn==1.1.3 joblib==1.3.1 catboost==1.2; 'arm' not in platform_machine -lightgbm==3.3.5 +lightgbm==4.0.0 xgboost==1.7.6 tensorboard==2.13.0 datasieve==0.1.7 diff --git a/requirements.txt b/requirements.txt index 187380f78..32d2b63ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,20 +3,20 @@ numpy==1.24.3; python_version <= '3.8' pandas==2.0.3 pandas-ta==0.3.14b -ccxt==4.0.17 -cryptography==41.0.1; platform_machine != 'armv7l' +ccxt==4.0.36 +cryptography==41.0.2; platform_machine != 'armv7l' cryptography==40.0.1; platform_machine == 'armv7l' -aiohttp==3.8.4 -SQLAlchemy==2.0.18 +aiohttp==3.8.5 +SQLAlchemy==2.0.19 python-telegram-bot==20.4 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 arrow==1.2.3 cachetools==5.3.1 requests==2.31.0 -urllib3==2.0.3 -jsonschema==4.18.0 -TA-Lib==0.4.26 +urllib3==2.0.4 +jsonschema==4.18.4 +TA-Lib==0.4.27 technical==1.4.0 tabulate==0.9.0 pycoingecko==3.1.0 @@ -25,7 +25,7 @@ tables==3.8.0 blosc==1.11.1 joblib==1.3.1 rich==13.4.2 -pyarrow==12.0.0; platform_machine != 'armv7l' +pyarrow==12.0.1; platform_machine != 'armv7l' # find first, C search in arrays py_find_1st==1.1.5 @@ -40,9 +40,9 @@ sdnotify==0.3.2 # API Server fastapi==0.100.0 -pydantic==1.10.9 -uvicorn==0.22.0 -pyjwt==2.7.0 +pydantic==1.10.11 +uvicorn==0.23.1 +pyjwt==2.8.0 aiofiles==23.1.0 psutil==5.9.5 diff --git a/setup.py b/setup.py index 1d35957e0..c2b725f87 100644 --- a/setup.py +++ b/setup.py @@ -97,7 +97,7 @@ setup( 'rich', 'pyarrow; platform_machine != "armv7l"', 'fastapi', - 'pydantic>=1.8.0', + 'pydantic>=1.8.0,<2.0', 'uvicorn', 'psutil', 'pyjwt', diff --git a/tests/conftest.py b/tests/conftest.py index 6c4a271d6..3d4f1e1e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3002,85 +3002,85 @@ def mark_ohlcv(): def funding_rate_history_hourly(): return [ { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": -0.000008, "timestamp": 1630454400000, "datetime": "2021-09-01T00:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": -0.000004, "timestamp": 1630458000000, "datetime": "2021-09-01T01:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0.000012, "timestamp": 1630461600000, "datetime": "2021-09-01T02:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": -0.000003, "timestamp": 1630465200000, "datetime": "2021-09-01T03:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": -0.000007, "timestamp": 1630468800000, "datetime": "2021-09-01T04:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0.000003, "timestamp": 1630472400000, "datetime": "2021-09-01T05:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0.000019, "timestamp": 1630476000000, "datetime": "2021-09-01T06:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0.000003, "timestamp": 1630479600000, "datetime": "2021-09-01T07:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": -0.000003, "timestamp": 1630483200000, "datetime": "2021-09-01T08:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0, "timestamp": 1630486800000, "datetime": "2021-09-01T09:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0.000013, "timestamp": 1630490400000, "datetime": "2021-09-01T10:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0.000077, "timestamp": 1630494000000, "datetime": "2021-09-01T11:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0.000072, "timestamp": 1630497600000, "datetime": "2021-09-01T12:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": 0.000097, "timestamp": 1630501200000, "datetime": "2021-09-01T13:00:00.000Z" @@ -3092,13 +3092,13 @@ def funding_rate_history_hourly(): def funding_rate_history_octohourly(): return [ { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": -0.000008, "timestamp": 1630454400000, "datetime": "2021-09-01T00:00:00.000Z" }, { - "symbol": "ADA/USDT", + "symbol": "ADA/USDT:USDT", "fundingRate": -0.000003, "timestamp": 1630483200000, "datetime": "2021-09-01T08:00:00.000Z" diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 5e377f851..c1b007e77 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -343,12 +343,24 @@ def test_calculate_expectancy(testdatadir): filename = testdatadir / "backtest_results/backtest-result.json" bt_data = load_backtest_data(filename) - expectancy = calculate_expectancy(DataFrame()) + expectancy, expectancy_ratio = calculate_expectancy(DataFrame()) assert expectancy == 0.0 + assert expectancy_ratio == 100 - expectancy = calculate_expectancy(bt_data) + expectancy, expectancy_ratio = calculate_expectancy(bt_data) assert isinstance(expectancy, float) - assert pytest.approx(expectancy) == 0.07151374226574791 + assert isinstance(expectancy_ratio, float) + assert pytest.approx(expectancy) == 5.820687070932315e-06 + assert pytest.approx(expectancy_ratio) == 0.07151374226574791 + + data = { + 'profit_abs': [100, 200, 50, -150, 300, -100, 80, -30] + } + df = DataFrame(data) + expectancy, expectancy_ratio = calculate_expectancy(df) + + assert pytest.approx(expectancy) == 56.25 + assert pytest.approx(expectancy_ratio) == 0.60267857 def test_calculate_sortino(testdatadir): diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 79e971b0d..6a2cb5638 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -135,6 +135,73 @@ def test_ohlcv_fill_up_missing_data2(caplog): f"{len(data)} - after: {len(data2)}.*", caplog) +def test_ohlcv_to_dataframe_1M(): + + # Monthly ticks from 2019-09-01 to 2023-07-01 + ticks = [ + [1567296000000, 8042.08, 10475.54, 7700.67, 8041.96, 608742.1109999999], + [1569888000000, 8285.31, 10408.48, 7172.76, 9150.0, 2439561.887], + [1572566400000, 9149.88, 9550.0, 6510.19, 7542.93, 4042674.725], + [1575158400000, 7541.08, 7800.0, 6427.0, 7189.0, 4063882.296], + [1577836800000, 7189.43, 9599.0, 6863.44, 9364.51, 5165281.358], + [1580515200000, 9364.5, 10540.0, 8450.0, 8531.98, 4581788.124], + [1583020800000, 8532.5, 9204.0, 3621.81, 6407.1, 10859497.479], + [1585699200000, 6407.1, 9479.77, 6140.0, 8624.76, 11276526.968], + [1588291200000, 8623.61, 10080.0, 7940.0, 9446.43, 12469561.02], + [1590969600000, 9446.49, 10497.25, 8816.4, 9138.87, 6684044.201], + [1593561600000, 9138.88, 11488.0, 8900.0, 11343.68, 5709327.926], + [1596240000000, 11343.67, 12499.42, 10490.0, 11658.11, 6746487.129], + [1598918400000, 11658.11, 12061.07, 9808.58, 10773.0, 6442697.051], + [1601510400000, 10773.0, 14140.0, 10371.03, 13783.73, 7404103.004], + [1604188800000, 13783.73, 19944.0, 13195.0, 19720.0, 12328272.549], + [1606780800000, 19722.09, 29376.7, 17555.0, 28951.68, 10067314.24], + [1609459200000, 28948.19, 42125.51, 27800.0, 33126.21, 12408873.079], + [1612137600000, 33125.11, 58472.14, 32322.47, 45163.36, 8784474.482], + [1614556800000, 45162.64, 61950.0, 44972.49, 58807.24, 9459821.267], + [1617235200000, 58810.99, 64986.11, 46930.43, 57684.16, 7895051.389], + [1619827200000, 57688.29, 59654.0, 28688.0, 37243.38, 16790964.443], + [1622505600000, 37244.36, 41413.0, 28780.01, 35031.39, 23474519.886], + [1625097600000, 35031.39, 48168.6, 29242.24, 41448.11, 16932491.175], + [1627776000000, 41448.1, 50600.0, 37291.0, 47150.32, 13645800.254], + [1630454400000, 47150.32, 52950.0, 39503.58, 43796.57, 10734742.869], + [1633046400000, 43799.49, 67150.0, 43260.01, 61348.61, 9111112.847], + [1635724800000, 61347.14, 69198.7, 53245.0, 56975.0, 7111424.463], + [1638316800000, 56978.06, 59100.0, 40888.89, 46210.56, 8404449.024], + [1640995200000, 46210.57, 48000.0, 32853.83, 38439.04, 11047479.277], + [1643673600000, 38439.04, 45847.5, 34303.7, 43155.0, 10910339.91], + [1646092800000, 43155.0, 48200.0, 37134.0, 45506.0, 10459721.586], + [1648771200000, 45505.9, 47448.0, 37550.0, 37614.5, 8463568.862], + [1651363200000, 37614.4, 40071.7, 26631.0, 31797.8, 14463715.774], + [1654041600000, 31797.9, 31986.1, 17593.2, 19923.5, 20710810.306], + [1656633600000, 19923.3, 24700.0, 18780.1, 23290.1, 20582518.513], + [1659312000000, 23290.1, 25200.0, 19508.0, 20041.5, 17221921.557], + [1661990400000, 20041.4, 22850.0, 18084.3, 19411.7, 21935261.414], + [1664582400000, 19411.6, 21088.0, 17917.8, 20482.0, 16625843.584], + [1667260800000, 20482.1, 21473.7, 15443.2, 17153.3, 18460614.013], + [1669852800000, 17153.4, 18400.0, 16210.0, 16537.6, 9702408.711], + [1672531200000, 16537.5, 23962.7, 16488.0, 23119.4, 14732180.645], + [1675209600000, 23119.5, 25347.6, 21338.0, 23129.6, 15025197.415], + [1677628800000, 23129.7, 29184.8, 19521.6, 28454.9, 23317458.541], + [1680307200000, 28454.8, 31059.0, 26919.3, 29223.0, 14654208.219], + [1682899200000, 29223.0, 29840.0, 25751.0, 27201.1, 13328157.284], + [1685577600000, 27201.1, 31500.0, 24777.0, 30460.2, 14099299.273], + [1688169600000, 30460.2, 31850.0, 28830.0, 29338.8, 8760361.377] + ] + + data = ohlcv_to_dataframe(ticks, '1M', pair="UNITTEST/USDT", + fill_missing=False, drop_incomplete=False) + assert len(data) == len(ticks) + assert data.iloc[0]['date'].strftime('%Y-%m-%d') == '2019-09-01' + assert data.iloc[-1]['date'].strftime('%Y-%m-%d') == '2023-07-01' + + # Test with filling missing data + data = ohlcv_to_dataframe(ticks, '1M', pair="UNITTEST/USDT", + fill_missing=True, drop_incomplete=False) + assert len(data) == len(ticks) + assert data.iloc[0]['date'].strftime('%Y-%m-%d') == '2019-09-01' + assert data.iloc[-1]['date'].strftime('%Y-%m-%d') == '2023-07-01' + + def test_ohlcv_drop_incomplete(caplog): timeframe = '1d' ticks = [ diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 51d016d11..c57e32633 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -544,6 +544,8 @@ class TestCCXTExchange: if exchangename in ('bittrex'): # For some weired reason, this test returns random lengths for bittrex. pytest.skip("Exchange doesn't provide stable ohlcv history") + if exchangename in ('bitvavo'): + pytest.skip("Exchange Downtime ") if not exc._ft_has['ohlcv_has_history']: pytest.skip("Exchange does not support candle history") diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5fa2755d2..289a67c4a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4343,11 +4343,11 @@ def test__fetch_and_calculate_funding_fees( ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) mocker.patch(f'{EXMS}.timeframes', PropertyMock(return_value=['1h', '4h', '8h'])) funding_fees = ex._fetch_and_calculate_funding_fees( - pair='ADA/USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) + pair='ADA/USDT:USDT', amount=amount, is_short=True, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == expected_fees # Fees for Longs are inverted funding_fees = ex._fetch_and_calculate_funding_fees( - pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) + pair='ADA/USDT:USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) assert pytest.approx(funding_fees) == -expected_fees # Return empty "refresh_latest" @@ -4355,7 +4355,7 @@ def test__fetch_and_calculate_funding_fees( ex = get_patched_exchange(mocker, default_conf, api_mock, id=exchange) with pytest.raises(ExchangeError, match="Could not find funding rates."): ex._fetch_and_calculate_funding_fees( - pair='ADA/USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) + pair='ADA/USDT:USDT', amount=amount, is_short=False, open_date=d1, close_date=d2) @pytest.mark.parametrize('exchange,expected_fees', [ @@ -5424,7 +5424,7 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun assert api_mock.create_order.call_args_list[0][1]['amount'] == order_amount assert order['amount'] == 100 - assert order['cost'] == 100 + assert order['cost'] == order_amount assert order['filled'] == 100 assert order['remaining'] == 100 diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index a333cda9d..cc1d9e95b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -601,6 +601,9 @@ def test_backtest__enter_trade_futures(default_conf_usdt, fee, mocker) -> None: trade = backtesting._enter_trade(pair, row=row, direction='short') assert pytest.approx(trade.liquidation_price) == 0.11787191 + assert pytest.approx(trade.orders[0].cost) == ( + trade.stake_amount * trade.leverage + trade.fee_open) + assert pytest.approx(trade.orders[-1].stake_amount) == trade.stake_amount # Stake-amount too high! mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=600.0) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 7b85e7978..2ea6a33a4 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -23,7 +23,8 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera store_backtest_analysis_results, store_backtest_stats, text_table_bt_results, text_table_exit_reason, text_table_strategy) -from freqtrade.optimize.optimize_reports.optimize_reports import _get_resample_from_period +from freqtrade.optimize.optimize_reports.optimize_reports import (_get_resample_from_period, + calc_streak) from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.util import dt_ts from freqtrade.util.datetime_helpers import dt_from_ts, dt_utc @@ -348,6 +349,32 @@ def test_generate_trading_stats(testdatadir): assert res['losses'] == 0 +def test_calc_streak(testdatadir): + df = pd.DataFrame({ + 'profit_ratio': [0.05, -0.02, -0.03, -0.05, 0.01, 0.02, 0.03, 0.04, -0.02, -0.03], + }) + # 4 consecutive wins, 3 consecutive losses + res = calc_streak(df) + assert res == (4, 3) + assert isinstance(res[0], int) + assert isinstance(res[1], int) + + # invert situation + df1 = df.copy() + df1['profit_ratio'] = df1['profit_ratio'] * -1 + assert calc_streak(df1) == (3, 4) + + df_empty = pd.DataFrame({ + 'profit_ratio': [], + }) + assert df_empty.empty + assert calc_streak(df_empty) == (0, 0) + + filename = testdatadir / "backtest_results/backtest-result.json" + bt_data = load_backtest_data(filename) + assert calc_streak(bt_data) == (7, 18) + + def test_text_table_exit_reason(): results = pd.DataFrame( diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 4aa3b1e96..958db8c72 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -563,14 +563,14 @@ def test_calc_open_close_trade_price( trade.open_order_id = f'something-{is_short}-{lev}-{exchange}' oobj = Order.parse_from_ccxt_object(entry_order, 'ADA/USDT', trade.entry_side) - oobj.trade = trade + oobj._trade_live = trade oobj.update_from_ccxt_object(entry_order) trade.update_trade(oobj) trade.funding_fees = funding_fees oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', trade.exit_side) - oobj.trade = trade + oobj._trade_live = trade oobj.update_from_ccxt_object(exit_order) trade.update_trade(oobj) diff --git a/tests/persistence/test_trade_fromjson.py b/tests/persistence/test_trade_fromjson.py index 2edfd18b8..24522e744 100644 --- a/tests/persistence/test_trade_fromjson.py +++ b/tests/persistence/test_trade_fromjson.py @@ -179,6 +179,7 @@ def test_trade_fromjson(): assert trade.open_date_utc == datetime(2022, 10, 18, 9, 12, 42, tzinfo=timezone.utc) assert isinstance(trade.open_date, datetime) assert trade.exit_reason == 'no longer good' + assert trade.realized_profit == 2.76315361 assert len(trade.orders) == 5 last_o = trade.orders[-1] diff --git a/tests/plugins/test_remotepairlist.py b/tests/plugins/test_remotepairlist.py index 5e6f5cbf1..9814e5662 100644 --- a/tests/plugins/test_remotepairlist.py +++ b/tests/plugins/test_remotepairlist.py @@ -35,7 +35,7 @@ def test_gen_pairlist_with_local_file(mocker, rpl_config): mock_file_path.exists.return_value = True jsonparse = json.loads(mock_file.read.return_value) - mocker.patch('freqtrade.plugins.pairlist.RemotePairList.json.load', return_value=jsonparse) + mocker.patch('freqtrade.plugins.pairlist.RemotePairList.rapidjson.load', return_value=jsonparse) rpl_config['pairlists'] = [ { diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5dfeeb632..3bc725f3a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -402,6 +402,8 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert res['first_trade_timestamp'] == 0 assert res['latest_trade_date'] == '' assert res['latest_trade_timestamp'] == 0 + assert res['expectancy'] == 0 + assert res['expectancy_ratio'] == 100 # Create some test data create_mock_trades_usdt(fee) @@ -413,6 +415,9 @@ def test_rpc_trade_statistics(default_conf_usdt, ticker, fee, mocker) -> None: assert pytest.approx(stats['profit_all_coin']) == -77.45964918 assert pytest.approx(stats['profit_all_percent_mean']) == -57.86 assert pytest.approx(stats['profit_all_fiat']) == -85.205614098 + assert pytest.approx(stats['winrate']) == 0.666666667 + assert pytest.approx(stats['expectancy']) == 0.913333333 + assert pytest.approx(stats['expectancy_ratio']) == 0.223308883 assert stats['trade_count'] == 7 assert stats['first_trade_humanized'] == '2 days ago' assert stats['latest_trade_humanized'] == '17 minutes ago' diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8b70a7e7e..abbaa421e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -829,7 +829,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): 'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015, 'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06, 'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2, - 'profit_factor': 0.0, 'trading_volume': 91.074, + 'profit_factor': 0.0, 'winrate': 0.0, 'expectancy': -0.0033695635, + 'expectancy_ratio': -1.0, 'trading_volume': 91.074, } ), ( @@ -844,7 +845,8 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): 'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015, 'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07, 'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0, - 'profit_factor': None, 'trading_volume': 91.074, + 'profit_factor': None, 'winrate': 1.0, 'expectancy': 0.0003695635, + 'expectancy_ratio': 100, 'trading_volume': 91.074, } ), ( @@ -859,7 +861,9 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): 'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005, 'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06, 'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1, - 'profit_factor': 0.02775724835771106, 'trading_volume': 91.074, + 'profit_factor': 0.02775724835771106, 'winrate': 0.5, + 'expectancy': -0.0027145635000000003, 'expectancy_ratio': -0.48612137582114445, + 'trading_volume': 91.074, } ) ]) @@ -916,6 +920,9 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) 'winning_trades': expected['winning_trades'], 'losing_trades': expected['losing_trades'], 'profit_factor': expected['profit_factor'], + 'winrate': expected['winrate'], + 'expectancy': expected['expectancy'], + 'expectancy_ratio': expected['expectancy_ratio'], 'max_drawdown': ANY, 'max_drawdown_abs': ANY, 'trading_volume': expected['trading_volume'], @@ -1469,30 +1476,47 @@ def test_api_pair_history(botclient, mocker): "&timerange=20180111-20180112") assert_response(rc, 422) + # Invalid strategy + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + "&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}11") + 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) - assert rc.json()['length'] == 289 - assert len(rc.json()['data']) == rc.json()['length'] - assert 'columns' in rc.json() - assert 'data' in rc.json() + 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 28 columns + assert len(result['columns']) == 28 + assert len(data[0]) == 28 + 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-11 00:00:00' + assert data[0][rsi_col_idx] is not None + assert data[0][rsi_col_idx] > 0 assert lfm.call_count == 1 - assert rc.json()['pair'] == 'UNITTEST/BTC' - assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY - assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' - assert rc.json()['data_start_ts'] == 1515628800000 - assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00' - assert rc.json()['data_stop_ts'] == 1515715200000 + 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()['error'] == ("Error querying /api/v1/pair_history: " - "No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") + assert rc.json()['detail'] == ("No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") def test_api_plot_config(botclient, mocker): @@ -1529,6 +1553,10 @@ def test_api_plot_config(botclient, mocker): assert_response(rc) assert rc.json()['subplots'] == {} + rc = client_get(client, f"{BASE_URI}/plot_config?strategy=NotAStrategy") + assert_response(rc, 502) + assert rc.json()['detail'] is not None + mocker.patch('freqtrade.rpc.api_server.api_v1.get_rpc_optional', return_value=None) rc = client_get(client, f"{BASE_URI}/plot_config") @@ -1981,7 +2009,7 @@ def test_api_backtest_history(botclient, mocker, testdatadir): result = rc.json() assert len(result) == 3 fn = result[0]['filename'] - assert fn == "backtest-result_multistrat.json" + assert fn == "backtest-result_multistrat" strategy = result[0]['strategy'] rc = client_get(client, f"{BASE_URI}/backtest/history/result?filename={fn}&strategy={strategy}") assert_response(rc) @@ -1995,6 +2023,34 @@ def test_api_backtest_history(botclient, mocker, testdatadir): assert result2['backtest_result']['strategy'][strategy] +def test_api_delete_backtest_history_entry(botclient, mocker, 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.json" + file_path.touch() + meta_path = file_path.with_suffix('.meta.json') + meta_path.touch() + + rc = client_delete(client, f"{BASE_URI}/backtest/history/randomFile.json") + assert_response(rc, 503) + assert rc.json()['detail'] == 'Bot is not in the correct state.' + + ftbot.config['user_data_dir'] = tmp_path + ftbot.config['runmode'] = RunMode.WEBSERVER + rc = client_delete(client, f"{BASE_URI}/backtest/history/randomFile.json") + assert rc.status_code == 404 + assert rc.json()['detail'] == 'File not found.' + + rc = client_delete(client, f"{BASE_URI}/backtest/history/{file_path.name}") + assert rc.status_code == 200 + + assert not file_path.exists() + assert not meta_path.exists() + + def test_health(botclient): ftbot, client = botclient diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 51879f5ad..72d70bfb9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -799,6 +799,8 @@ async def test_telegram_profit_handle( assert '*Best Performing:* `ETH/USDT: 9.45%`' in msg_mock.call_args_list[-1][0][0] assert '*Max Drawdown:*' in msg_mock.call_args_list[-1][0][0] assert '*Profit factor:*' in msg_mock.call_args_list[-1][0][0] + assert '*Winrate:*' in msg_mock.call_args_list[-1][0][0] + assert '*Expectancy (Ratio):*' in msg_mock.call_args_list[-1][0][0] assert '*Trading volume:* `126 USDT`' in msg_mock.call_args_list[-1][0][0] diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index d0a0f5b1e..36b96ace5 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -381,7 +381,7 @@ def test__send_msg(default_conf, mocker, caplog): webhook._send_msg(msg) assert post.call_count == 1 - assert post.call_args[1] == {'data': msg} + assert post.call_args[1] == {'data': msg, 'timeout': 10} assert post.call_args[0] == (default_conf['webhook']['url'], ) post = MagicMock(side_effect=RequestException) @@ -399,7 +399,7 @@ def test__send_msg_with_json_format(default_conf, mocker, caplog): mocker.patch("freqtrade.rpc.webhook.post", post) webhook._send_msg(msg) - assert post.call_args[1] == {'json': msg} + assert post.call_args[1] == {'json': msg, 'timeout': 10} def test__send_msg_with_raw_format(default_conf, mocker, caplog): @@ -411,7 +411,11 @@ def test__send_msg_with_raw_format(default_conf, mocker, caplog): mocker.patch("freqtrade.rpc.webhook.post", post) webhook._send_msg(msg) - assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}} + assert post.call_args[1] == { + 'data': msg['data'], + 'headers': {'Content-Type': 'text/plain'}, + 'timeout': 10 + } def test_send_msg_discord(default_conf, mocker): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3bfa5a127..e533acbb8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2793,7 +2793,7 @@ def test_manage_open_orders_entry( freqtrade.strategy.check_entry_timeout = MagicMock(return_value=False) freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234) - # check it does cancel buy orders over the time limit + # check it does cancel entry orders over the time limit freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 2 @@ -2801,7 +2801,7 @@ def test_manage_open_orders_entry( select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() nb_trades = len(trades) assert nb_trades == 0 - # Custom user buy-timeout is never called + # Custom user entry-timeout is never called assert freqtrade.strategy.check_entry_timeout.call_count == 0 # Entry adjustment is never called assert freqtrade.strategy.adjust_entry_price.call_count == 0 @@ -5023,7 +5023,7 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker, (8.0, 0.1, 8.0, None), (8.0, 0.1, 7.9, 0.1), ]) -def test_apply_fee_conditional(default_conf_usdt, fee, mocker, +def test_apply_fee_conditional(default_conf_usdt, fee, mocker, caplog, amount, fee_abs, wallet, amount_exp): walletmock = mocker.patch('freqtrade.wallets.Wallets.update') mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=wallet) @@ -5048,6 +5048,60 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker, # Amount is kept as is assert freqtrade.apply_fee_conditional(trade, 'LTC', amount, fee_abs, order) == amount_exp assert walletmock.call_count == 1 + if fee_abs != 0 and amount_exp is None: + assert log_has_re(r"Fee amount.*Eating.*dust\.", caplog) + + +@pytest.mark.parametrize('amount,fee_abs,wallet,amount_exp', [ + (8.0, 0.0, 16, None), + (8.0, 0.0, 0, None), + (8.0, 0.1, 8, 0.1), + (8.0, 0.1, 20, None), + (8.0, 0.1, 16.0, None), + (8.0, 0.1, 7.9, 0.1), + (8.0, 0.1, 12, 0.1), + (8.0, 0.1, 15.9, 0.1), +]) +def test_apply_fee_conditional_multibuy(default_conf_usdt, fee, mocker, caplog, + amount, fee_abs, wallet, amount_exp): + walletmock = mocker.patch('freqtrade.wallets.Wallets.update') + mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=wallet) + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_order_id="123456" + ) + # One closed order + order = Order( + ft_order_side='buy', + order_id='10', + ft_pair=trade.pair, + ft_is_open=False, + filled=amount, + status="closed" + ) + trade.orders.append(order) + # Add additional order - this should NOT eat into dust unless the wallet was bigger already. + order1 = Order( + ft_order_side='buy', + order_id='100', + ft_pair=trade.pair, + ft_is_open=True, + ) + trade.orders.append(order1) + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + + walletmock.reset_mock() + # The new trade amount will be 2x amount - fee / wallet will have to be adapted to this. + assert freqtrade.apply_fee_conditional(trade, 'LTC', amount, fee_abs, order1) == amount_exp + assert walletmock.call_count == 1 + if fee_abs != 0 and amount_exp is None: + assert log_has_re(r"Fee amount.*Eating.*dust\.", caplog) @pytest.mark.parametrize("delta, is_high_delta", [ diff --git a/tests/test_integration.py b/tests/test_integration.py index 2949f1ef2..41265ae74 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -429,6 +429,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert pytest.approx(trade.stop_loss) == 1.99 * (1 - 0.1 / leverage) assert pytest.approx(trade.initial_stop_loss) == 1.96 * (1 - 0.1 / leverage) assert trade.initial_stop_loss_pct == -0.1 + assert pytest.approx(trade.orders[-1].stake_amount) == trade.stake_amount # 2nd order - not filling freqtrade.strategy.adjust_trade_position = MagicMock(return_value=120) @@ -473,13 +474,38 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert pytest.approx(trade.orders[1].amount) == 30.150753768 * leverage assert pytest.approx(trade.orders[-1].amount) == 61.538461232 * leverage + # Full exit + mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) + freqtrade.strategy.custom_exit = MagicMock(return_value='Exit now') + freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.02) + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 5 + assert trade.orders[-1].side == trade.exit_side + assert trade.orders[-1].status == 'open' + assert trade.orders[-1].price == 2.02 + assert pytest.approx(trade.amount) == 91.689215 * leverage + assert pytest.approx(trade.orders[-1].amount) == 91.689215 * leverage + assert freqtrade.strategy.adjust_entry_price.call_count == 0 + # Process again, should not adjust entry price + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 5 + assert trade.orders[-1].status == 'open' + assert trade.orders[-1].price == 2.02 + # Adjust entry price cannot be called - this is an exit order + assert freqtrade.strategy.adjust_entry_price.call_count == 0 + @pytest.mark.parametrize('leverage', [1, 2]) def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, leverage) -> None: default_conf_usdt['position_adjustment_enable'] = True - + spot = leverage == 1 + if not spot: + default_conf_usdt['trading_mode'] = 'futures' + default_conf_usdt['margin_mode'] = 'isolated' freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - freqtrade.trading_mode = TradingMode.FUTURES + assert freqtrade.trading_mode == TradingMode.FUTURES if not spot else TradingMode.SPOT mocker.patch.multiple( EXMS, fetch_ticker=ticker_usdt, @@ -487,8 +513,11 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, get_min_pair_stake_amount=MagicMock(return_value=10), + get_funding_fees=MagicMock(return_value=0), ) mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) + starting_amount = freqtrade.wallets.get_total('USDT') + assert starting_amount == 1000 patch_get_signal(freqtrade) freqtrade.strategy.leverage = MagicMock(return_value=leverage) @@ -498,8 +527,14 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera trade = Trade.get_trades().first() assert len(trade.orders) == 1 assert pytest.approx(trade.stake_amount) == 60 + assert trade.leverage == leverage assert pytest.approx(trade.amount) == 30.0 * leverage assert trade.open_rate == 2.0 + assert pytest.approx(freqtrade.wallets.get_free('USDT')) == starting_amount - 60 + if spot: + assert pytest.approx(freqtrade.wallets.get_total('USDT')) == starting_amount - 60 + else: + assert freqtrade.wallets.get_total('USDT') == starting_amount # Too small size freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-59) @@ -521,6 +556,15 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera assert pytest.approx(trade.amount) == 20.099 * leverage assert trade.open_rate == 2.0 assert trade.is_open + assert trade.realized_profit > 0.098 * leverage + expected_profit = starting_amount - 40.1980 + trade.realized_profit + assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit + + if spot: + assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit + else: + # total won't change in futures mode, only free / used will. + assert freqtrade.wallets.get_total('USDT') == starting_amount + trade.realized_profit caplog.clear() # Sell more than what we got (we got ~20 coins left) @@ -545,3 +589,10 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera assert pytest.approx(trade.stake_amount) == 40.198 assert trade.is_open assert log_has_re('Amount to exit is 0.0 due to exchange limits - not exiting.', caplog) + expected_profit = starting_amount - 40.1980 + trade.realized_profit + assert pytest.approx(freqtrade.wallets.get_free('USDT')) == expected_profit + if spot: + assert pytest.approx(freqtrade.wallets.get_total('USDT')) == expected_profit + else: + # total won't change in futures mode, only free / used will. + assert freqtrade.wallets.get_total('USDT') == starting_amount + trade.realized_profit diff --git a/tests/test_misc.py b/tests/test_misc.py index 21c832c2c..3943e7f15 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -8,7 +8,7 @@ import pandas as pd import pytest from freqtrade.misc import (dataframe_to_json, decimals_per_coin, deep_merge_dicts, file_dump_json, - file_load_json, json_to_dataframe, pair_to_filename, + file_load_json, is_file_in_dir, json_to_dataframe, pair_to_filename, parse_db_uri_for_logging, plural, render_template, render_template_with_fallback, round_coin_value, safe_value_fallback, safe_value_fallback2) @@ -64,6 +64,24 @@ def test_file_load_json(mocker, testdatadir) -> None: assert ret +def test_is_file_in_dir(tmp_path): + + # Create a temporary directory and file + dir_path = tmp_path / "subdir" + dir_path.mkdir() + file_path = dir_path / "test.txt" + file_path.touch() + + # Test that the function returns True when the file is in the directory + assert is_file_in_dir(file_path, dir_path) is True + + # Test that the function returns False when the file is not in the directory + assert is_file_in_dir(file_path, tmp_path) is False + + file_path2 = tmp_path / "../../test2.txt" + assert is_file_in_dir(file_path2, tmp_path) is False + + @pytest.mark.parametrize("pair,expected_result", [ ("ETH/BTC", 'ETH_BTC'), ("ETH/USDT", 'ETH_USDT'), diff --git a/tests/test_wallets.py b/tests/test_wallets.py index c3ff4ccd0..478993058 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -8,7 +8,8 @@ from sqlalchemy import select from freqtrade.constants import UNLIMITED_STAKE_AMOUNT from freqtrade.exceptions import DependencyException from freqtrade.persistence import Trade -from tests.conftest import EXMS, create_mock_trades, get_patched_freqtradebot, patch_wallet +from tests.conftest import (EXMS, create_mock_trades, create_mock_trades_usdt, + get_patched_freqtradebot, patch_wallet) def test_sync_wallet_at_boot(mocker, default_conf): @@ -341,6 +342,33 @@ def test_sync_wallet_futures_live(mocker, default_conf): assert 'ETH/USDT:USDT' not in freqtrade.wallets._positions +def test_sync_wallet_dry(mocker, default_conf_usdt, fee): + default_conf_usdt['dry_run'] = True + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + assert len(freqtrade.wallets._wallets) == 1 + assert len(freqtrade.wallets._positions) == 0 + assert freqtrade.wallets.get_total('USDT') == 1000 + + create_mock_trades_usdt(fee, is_short=None) + + freqtrade.wallets.update() + + assert len(freqtrade.wallets._wallets) == 5 + assert len(freqtrade.wallets._positions) == 0 + bal = freqtrade.wallets.get_all_balances() + assert bal['NEO'].total == 10 + assert bal['XRP'].total == 10 + assert bal['LTC'].total == 2 + assert bal['USDT'].total == 922.74 + + assert freqtrade.wallets.get_starting_balance() == default_conf_usdt['dry_run_wallet'] + total = freqtrade.wallets.get_total('LTC') + free = freqtrade.wallets.get_free('LTC') + used = freqtrade.wallets.get_used('LTC') + assert free != 0 + assert free + used == total + + def test_sync_wallet_futures_dry(mocker, default_conf, fee): default_conf['dry_run'] = True default_conf['trading_mode'] = 'futures'