diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 912b906d7..a2f6567dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,7 +90,7 @@ jobs: - name: Backtesting (multi) run: | - cp config_examples/config_bittrex.example.json config.json + cp tests/testdata/config.tests.json config.json freqtrade create-userdir --userdir user_data freqtrade new-strategy -s AwesomeStrategy freqtrade new-strategy -s AwesomeStrategyMin --template minimal @@ -98,7 +98,7 @@ jobs: - name: Hyperopt run: | - cp config_examples/config_bittrex.example.json config.json + cp tests/testdata/config.tests.json config.json freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 6 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all @@ -200,14 +200,14 @@ jobs: - name: Backtesting run: | - cp config_examples/config_bittrex.example.json config.json + cp tests/testdata/config.tests.json config.json freqtrade create-userdir --userdir user_data freqtrade new-strategy -s AwesomeStrategyAdv --template advanced freqtrade backtesting --datadir tests/testdata --strategy AwesomeStrategyAdv - name: Hyperopt run: | - cp config_examples/config_bittrex.example.json config.json + cp tests/testdata/config.tests.json config.json freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all @@ -275,13 +275,13 @@ jobs: - name: Backtesting run: | - cp config_examples/config_bittrex.example.json config.json + cp tests/testdata/config.tests.json config.json freqtrade create-userdir --userdir user_data freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy - name: Hyperopt run: | - cp config_examples/config_bittrex.example.json config.json + cp tests/testdata/config.tests.json config.json freqtrade create-userdir --userdir user_data freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7f150af3..0c6575e1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,17 +8,17 @@ repos: # stages: [push] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.5.1" + rev: "v1.7.0" hooks: - id: mypy exclude: build_helpers additional_dependencies: - - types-cachetools==5.3.0.6 + - types-cachetools==5.3.0.7 - types-filelock==3.2.7 - types-requests==2.31.0.10 - types-tabulate==0.9.0.3 - types-python-dateutil==2.8.19.14 - - SQLAlchemy==2.0.22 + - SQLAlchemy==2.0.23 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 040aae39c..b0d8f337b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,7 +125,7 @@ Exceptions: Contributors may be given commit privileges. Preference will be given to those with: -1. Past contributions to Freqtrade and other related open-source projects. Contributions to Freqtrade include both code (both accepted and pending) and friendly participation in the issue tracker and Pull request reviews. Quantity and quality are considered. +1. Past contributions to Freqtrade and other related open-source projects. Contributions to Freqtrade include both code (both accepted and pending) and friendly participation in the issue tracker and Pull request reviews. Both quantity and quality are considered. 1. A coding style that the other core committers find simple, minimal, and clean. 1. Access to resources for cross-platform development and testing. 1. Time to devote to the project regularly. diff --git a/Dockerfile b/Dockerfile index 7a4e85812..38f9ca788 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11.5-slim-bullseye as base +FROM python:3.11.6-slim-bookworm as base # Setup env ENV LANG C.UTF-8 diff --git a/README.md b/README.md index 0cacfe703..c00d2c999 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ hesitate to read the source code and understand the mechanism of this bot. Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange. - [X] [Binance](https://www.binance.com/) -- [X] [Bittrex](https://bittrex.com/) +- [X] [Bitmart](https://bitmart.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Huobi](http://huobi.com/) - [X] [Kraken](https://kraken.com/) diff --git a/build_helpers/TA_Lib-0.4.28-cp311-cp311-linux_armv7l.whl b/build_helpers/TA_Lib-0.4.28-cp311-cp311-linux_armv7l.whl new file mode 100644 index 000000000..f88bc8002 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.28-cp311-cp311-linux_armv7l.whl differ diff --git a/build_helpers/TA_Lib-0.4.28-cp39-cp39-linux_armv7l.whl b/build_helpers/TA_Lib-0.4.28-cp39-cp39-linux_armv7l.whl new file mode 100644 index 000000000..596b5923d Binary files /dev/null and b/build_helpers/TA_Lib-0.4.28-cp39-cp39-linux_armv7l.whl differ diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 8f0de2cc9..71fc01509 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -54,7 +54,7 @@ docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM # Run backtest -docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 +docker run --rm -v $(pwd)/tests/testdata/config.tests.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index 72b20ac5d..34dc1faa3 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -67,7 +67,7 @@ docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI docker tag freqtrade:$TAG_FREQAI_RL ${CACHE_IMAGE}:$TAG_FREQAI_RL # Run backtest -docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 +docker run --rm -v $(pwd)/tests/testdata/config.tests.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/build_helpers/pyarrow-14.0.1-cp311-cp311-linux_armv7l.whl b/build_helpers/pyarrow-14.0.1-cp311-cp311-linux_armv7l.whl new file mode 100644 index 000000000..46529fbe4 Binary files /dev/null and b/build_helpers/pyarrow-14.0.1-cp311-cp311-linux_armv7l.whl differ diff --git a/build_helpers/pyarrow-13.0.0-cp39-cp39-linux_armv7l.whl b/build_helpers/pyarrow-14.0.1-cp39-cp39-linux_armv7l.whl similarity index 63% rename from build_helpers/pyarrow-13.0.0-cp39-cp39-linux_armv7l.whl rename to build_helpers/pyarrow-14.0.1-cp39-cp39-linux_armv7l.whl index 6dcca7db8..08226b794 100644 Binary files a/build_helpers/pyarrow-13.0.0-cp39-cp39-linux_armv7l.whl and b/build_helpers/pyarrow-14.0.1-cp39-cp39-linux_armv7l.whl differ diff --git a/config_examples/config_binance.example.json b/config_examples/config_binance.example.json index 7968bdedc..3a2cea530 100644 --- a/config_examples/config_binance.example.json +++ b/config_examples/config_binance.example.json @@ -1,6 +1,6 @@ { "max_open_trades": 3, - "stake_currency": "BTC", + "stake_currency": "USDT", "stake_amount": 0.05, "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", @@ -36,21 +36,21 @@ "ccxt_async_config": { }, "pair_whitelist": [ - "ALGO/BTC", - "ATOM/BTC", - "BAT/BTC", - "BCH/BTC", - "BRD/BTC", - "EOS/BTC", - "ETH/BTC", - "IOTA/BTC", - "LINK/BTC", - "LTC/BTC", - "NEO/BTC", - "NXS/BTC", - "XMR/BTC", - "XRP/BTC", - "XTZ/BTC" + "ALGO/USDT", + "ATOM/USDT", + "BAT/USDT", + "BCH/USDT", + "BRD/USDT", + "EOS/USDT", + "ETH/USDT", + "IOTA/USDT", + "LINK/USDT", + "LTC/USDT", + "NEO/USDT", + "NXS/USDT", + "XMR/USDT", + "XRP/USDT", + "XTZ/USDT" ], "pair_blacklist": [ "BNB/.*" diff --git a/docker/Dockerfile.armhf b/docker/Dockerfile.armhf index 9e518a01d..c8efa4232 100644 --- a/docker/Dockerfile.armhf +++ b/docker/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM python:3.9.16-slim-bullseye as base +FROM python:3.11.6-slim-bookworm as base # Setup env ENV LANG C.UTF-8 @@ -11,34 +11,31 @@ ENV FT_APP_ENV="docker" # Prepare environment RUN mkdir /freqtrade \ && apt-get update \ - && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-dev libutf8proc-dev libsnappy-dev \ + && apt-get -y install sudo libatlas3-base libopenblas-dev curl sqlite3 libhdf5-dev libutf8proc-dev libsnappy-dev \ && apt-get clean \ && useradd -u 1000 -G sudo -U -m ftuser \ && chown ftuser:ftuser /freqtrade \ # Allow sudoers - && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers + && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \ + && pip install --upgrade pip WORKDIR /freqtrade # Install dependencies FROM base as python-deps RUN apt-get update \ - && apt-get -y install build-essential libssl-dev libffi-dev libopenblas-dev libgfortran5 pkg-config cmake gcc \ + && apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get clean \ - && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf # Install TA-lib COPY build_helpers/* /tmp/ -RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* -ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies COPY --chown=ftuser:ftuser requirements.txt /freqtrade/ USER ftuser -RUN pip install --user --no-cache-dir numpy==1.25.2 \ - && pip install --user /tmp/pyarrow-*.whl \ - && pip install --user --no-build-isolation TA-Lib==0.4.28 \ +RUN pip install --user --no-cache-dir numpy \ + && pip install --user --no-index --find-links /tmp/ pyarrow TA-Lib==0.4.28 \ && pip install --user --no-cache-dir -r requirements.txt # Copy dependencies to runtime-image diff --git a/docs/backtesting.md b/docs/backtesting.md index 41498fb00..d13b00a38 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -170,11 +170,11 @@ freqtrade backtesting --strategy AwesomeStrategy --dry-run-wallet 1000 Using a different on-disk historical candle (OHLCV) data source -Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory. +Assume you downloaded the history data from the Binance exchange and kept it in the `user_data/data/binance-20180101` directory. You can then use this data for backtesting as follows: ```bash -freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/bittrex-20180101 +freqtrade backtesting --strategy AwesomeStrategy --datadir user_data/data/binance-20180101 ``` --- diff --git a/docs/configuration.md b/docs/configuration.md index 7303f78c7..313e3e456 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -594,7 +594,7 @@ creating trades on the exchange. ```json "exchange": { - "name": "bittrex", + "name": "binance", "key": "key", "secret": "secret", ... @@ -644,7 +644,7 @@ API Keys are usually only required for live trading (trading for real money, bot ```json { "exchange": { - "name": "bittrex", + "name": "binance", "key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b", "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", //"password": "", // Optional, not needed by all exchanges) diff --git a/docs/developer.md b/docs/developer.md index 23f4a9b26..2a826d866 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -318,6 +318,7 @@ Additional tests / steps to complete: * Check if balance shows correctly (*) * Create market order (*) * Create limit order (*) +* Cancel order (*) * Complete trade (enter + exit) (*) * Compare result calculation between exchange and bot * Ensure fees are applied correctly (check the database against the exchange) diff --git a/docs/exchanges.md b/docs/exchanges.md index 237125e88..ac3957b07 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -302,6 +302,24 @@ We do strongly recommend to limit all API keys to the IP you're going to use it 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. +## Bitmart + +Bitmart requires the API key Memo (the name you give the API key) to go along with the exchange key and secret. +It's therefore required to pass the UID as well. + +```json +"exchange": { + "name": "bitmart", + "uid": "your_bitmart_api_key_memo", + "secret": "your_exchange_secret", + "password": "your_exchange_api_key_password", + // ... +} +``` + +!!! Warning "Necessary Verification" + Bitmart requires Verification Lvl2 to successfully trade on the spot market through the API - even though trading via UI works just fine with just Lvl1 verification. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index b7a30eb5a..6a1537d91 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -7,7 +7,7 @@ Low level feature engineering is performed in the user strategy within a set of | Function | Description | |---------------|-------------| | `feature_engineering_expand_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. -| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`. +| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `indicator_periods_candles`. | `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week). | `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals. diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 95687c7ab..905ea479a 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -74,7 +74,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | | **Reinforcement Learning Parameters within the `freqai.rl_config` sub dictionary** | `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model.
**Datatype:** Dictionary. | `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points.
**Datatype:** Integer. -| `cpu_count` | Number of processors to dedicate to the Reinforcement Learning training process.
**Datatype:** int. | `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function.
**Datatype:** int. | `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website)
**Datatype:** string. | `policy_type` | One of the available policy types from stable_baselines3
**Datatype:** string. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 3fbbee7f6..444279195 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -337,11 +337,15 @@ There are four parameter types each suited for different purposes. * `CategoricalParameter` - defines a parameter with a predetermined number of choices. * `BooleanParameter` - Shorthand for `CategoricalParameter([True, False])` - great for "enable" parameters. -!!! Tip "Disabling parameter optimization" - Each parameter takes two boolean parameters: - * `load` - when set to `False` it will not load values configured in `buy_params` and `sell_params`. - * `optimize` - when set to `False` parameter will not be included in optimization process. - Use these parameters to quickly prototype various ideas. +### Parameter options + +There are two parameter options that can help you to quickly test various ideas: + +* `optimize` - when set to `False`, the parameter will not be included in optimization process. (Default: True) +* `load` - when set to `False`, results of a previous hyperopt run (in `buy_params` and `sell_params` either in your strategy or the JSON output file) will not be used as the starting value for subsequent hyperopts. The default value specified in the parameter will be used instead. (Default: True) + +!!! Tip "Effects of `load=False` on backtesting" + Be aware that setting the `load` option to `False` will mean backtesting will also use the default value specified in the parameter and *not* the value found through hyperoptimisation. !!! Warning Hyperoptable parameters cannot be used in `populate_indicators` - as hyperopt does not recalculate indicators for each epoch, so the starting value would be used in this case. diff --git a/docs/index.md b/docs/index.md index 190e7e3c3..1df5424de 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,7 +40,7 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange. - [X] [Binance](https://www.binance.com/) -- [X] [Bittrex](https://bittrex.com/) +- [X] [Bitmart](https://bitmart.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Huobi](http://huobi.com/) - [X] [Kraken](https://kraken.com/) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 029dc4203..10c70939e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ -markdown==3.5 +markdown==3.5.1 mkdocs==1.5.3 -mkdocs-material==9.4.6 +mkdocs-material==9.4.14 mdx_truly_sane_lists==1.3 -pymdown-extensions==10.3.1 +pymdown-extensions==10.5 jinja2==3.1.2 diff --git a/docs/rest-api.md b/docs/rest-api.md index 666056a65..229fa5f94 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -134,13 +134,16 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `reload_config` | Reloads the configuration file. | `trades` | List last trades. Limited to 500 trades per call. | `trade/` | Get specific trade. -| `trade/` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. -| `trade//open-order` | DELETE - Cancel open order for this trade. -| `trade//reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange. +| `trades/` | DELETE - Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. +| `trades//open-order` | DELETE - Cancel open order for this trade. +| `trades//reload` | GET - Reload a trade from the Exchange. Only works in live, and can potentially help recover a trade that was manually sold on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. | `logs` | Shows last log messages. | `status` | Lists all open trades. | `count` | Displays number of trades used and available. +| `entries [pair]` | Shows profit statistics for each enter tags for given pair (or all pairs if pair isn't given). Pair is optional. +| `exits [pair]` | Shows profit statistics for each exit reasons for given pair (or all pairs if pair isn't given). Pair is optional. +| `mix_tags [pair]` | Shows profit statistics for each combinations of enter tag + exit reasons for given pair (or all pairs if pair isn't given). Pair is optional. | `locks` | Displays currently locked pairs. | `delete_lock ` | Deletes (disables) the lock by id. | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 2639d5521..34d72a6ef 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -760,9 +760,9 @@ The `position_adjustment_enable` strategy property enables the usage of `adjust_ For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. `adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging) or to increase or decrease positions. -`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys. +`max_entry_position_adjustment` property is used to limit the number of additional entries per trade (on top of the first entry order) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment entries. -The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased). +The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional entry order should be made (position is increased -> buy order for long trades, sell order for short trades). If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. Additional orders also result in additional fees and those orders don't count towards `max_open_trades`. @@ -770,9 +770,11 @@ This callback is **not** called when there is an open order (either buy or sell) `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. -Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. +Additional entries are ignored once you have reached the maximum amount of extra entries that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. -Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible, and the stake-amount is assumed to be before applying leverage. +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. + +Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index e2cdad81a..e80a30c64 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -173,7 +173,7 @@ You can use [recursive-analysis](recursive-analysis.md) to check and find the co In this example strategy, this should be set to 400 (`startup_candle_count = 400`), since the minimum needed history for ema100 calculation to make sure the value is correct is 400 candles. ``` python - dataframe['ema100'] = ta.EMA(dataframe, timeperiod=400) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) ``` By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt. @@ -486,17 +486,18 @@ for more information. :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use - current pair. + current pair. Also supports limited pair format strings (see below) :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: - * {base}_{quote}_{column}_{timeframe} if asset is specified. + * {base}_{quote}_{column}_{timeframe} if asset is specified. * {column}_{timeframe} if asset is not specified. - Format string supports these format variables: - * {asset} - full name of the asset, for example 'BTC/USDT'. + Pair format supports these format variables: * {base} - base currency in lower case, for example 'eth'. * {BASE} - same as {base}, except in upper case. * {quote} - quote currency in lower case, for example 'usdt'. * {QUOTE} - same as {quote}, except in upper case. + Format string additionally supports this variables. + * {asset} - full name of the asset, for example 'BTC/USDT'. * {column} - name of dataframe column. * {timeframe} - timeframe of informative dataframe. :param ffill: ffill dataframe after merging informative pair. diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 9e6f56e49..b423eca6e 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -570,7 +570,7 @@ def populate_any_indicators( ``` 1. Features - Move to `feature_engineering_expand_all` -2. Basic features, not expanded across `include_periods_candles` - move to`feature_engineering_expand_basic()`. +2. Basic features, not expanded across `indicator_periods_candles` - move to`feature_engineering_expand_basic()`. 3. Standard features which should not be expanded - move to `feature_engineering_standard()`. 4. Targets - Move this part to `set_freqai_targets()`. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f501d0e49..e4dc02c76 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -175,6 +175,7 @@ official commands. You can ask at any moment for help with `/help`. | `/status` | Lists all open trades | `/status ` | Lists one or more specific trade. Separate multiple with a blank space. | `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) +| `/order ` | Lists orders of one or more specific trade. Separate multiple with a blank space. | `/trades [limit]` | List all recently closed trades in a table format. | `/count` | Displays number of trades used and available | `/locks` | Show currently locked pairs. diff --git a/docs/utils.md b/docs/utils.md index 65ab50b9e..ac0d341bd 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -427,25 +427,33 @@ zb True missing opt: fetchMyTrades Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange. ``` -usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [-1] +usage: freqtrade list-timeframes [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] + [--exchange EXCHANGE] [-1] -optional arguments: +options: -h, --help show this help message and exit - --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. + --exchange EXCHANGE Exchange name. Only valid if no config is provided. -1, --one-column Print output in one column. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more details. + --logfile FILE, --log-file FILE + Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to `-` - to read config from stdin. - -d PATH, --datadir PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH, --data-dir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. + ``` * Example: see the timeframes for the 'binance' exchange, set in the configuration file: @@ -479,20 +487,17 @@ usage: freqtrade list-markets [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [--print-list] [--print-json] [-1] [--print-csv] [--base BASE_CURRENCY [BASE_CURRENCY ...]] - [--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a] - [--trading-mode {spot,margin,futures}] - + [--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] + [-a] [--trading-mode {spot,margin,futures}] usage: freqtrade list-pairs [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [--print-list] [--print-json] [-1] [--print-csv] [--base BASE_CURRENCY [BASE_CURRENCY ...]] [--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a] [--trading-mode {spot,margin,futures}] - -optional arguments: +options: -h, --help show this help message and exit - --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no - config is provided. + --exchange EXCHANGE Exchange name. Only valid if no config is provided. --print-list Print list of pairs or market symbols. By default data is printed in the tabular format. --print-json Print list of pairs or market symbols in JSON format. @@ -504,20 +509,22 @@ optional arguments: Specify quote currency(-ies). Space-separated list. -a, --all Print all pairs or market symbols. By default only active ones are shown. - --trading-mode {spot,margin,futures} + --trading-mode {spot,margin,futures}, --tradingmode {spot,margin,futures} Select Trading mode Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: + --logfile FILE, --log-file FILE + Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). - Multiple --config options may be used. Can be set to - `-` to read config from stdin. - -d PATH, --datadir PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH, --data-dir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. @@ -532,7 +539,7 @@ Pairs/markets are sorted by its symbol string in the printed output. ### Examples * Print the list of active pairs with quote currency USD on exchange, specified in the default -configuration file (i.e. pairs on the "Bittrex" exchange) in JSON format: +configuration file (i.e. pairs on the "Binance" exchange) in JSON format: ``` $ freqtrade list-pairs --quote USD --print-json @@ -564,7 +571,7 @@ usage: freqtrade test-pairlist [-h] [--userdir PATH] [-v] [-c PATH] [--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-1] [--print-json] [--exchange EXCHANGE] -optional arguments: +options: -h, --help show this help message and exit --userdir PATH, --user-data-dir PATH Path to userdata directory. @@ -578,8 +585,7 @@ optional arguments: Specify quote currency(-ies). Space-separated list. -1, --one-column Print output in one column. --print-json Print list of pairs or market symbols in JSON format. - --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no - config is provided. + --exchange EXCHANGE Exchange name. Only valid if no config is provided. ``` diff --git a/docs/webhook-config.md b/docs/webhook-config.md index e18a05e9b..811fa1b8e 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -302,6 +302,7 @@ You can configure this as follows: ``` The above represents the default (`exit_fill` and `entry_fill` are optional and will default to the above configuration) - modifications are obviously possible. +To disable either of the two default values (`entry_fill` / `exit_fill`), you can assign them an empty array (`exit_fill: []`). Available fields correspond to the fields for webhooks and are documented in the corresponding webhook sections. diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 6ff32e003..fc68d3027 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2023.10' +__version__ = '2023.11' 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 6692ad59a..2dff87a7d 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -108,7 +108,6 @@ def ask_user_config() -> Dict[str, Any]: "choices": [ "binance", "binanceus", - "bittrex", "gate", "huobi", "kraken", diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py index ac5893585..36587e573 100644 --- a/freqtrade/data/entryexitanalysis.py +++ b/freqtrade/data/entryexitanalysis.py @@ -211,8 +211,9 @@ def prepare_results(analysed_trades, stratname, timerange=None): res_df = pd.DataFrame() for pair, trades in analysed_trades[stratname].items(): - trades.dropna(subset=['close_date'], inplace=True) - res_df = pd.concat([res_df, trades], ignore_index=True) + if (trades.shape[0] > 0): + trades.dropna(subset=['close_date'], inplace=True) + res_df = pd.concat([res_df, trades], ignore_index=True) res_df = _select_rows_within_dates(res_df, timerange) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 9ac31a0d8..8de9120dc 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -4,6 +4,7 @@ from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_ from freqtrade.exchange.exchange import Exchange # isort: on from freqtrade.exchange.binance import Binance +from freqtrade.exchange.bitmart import Bitmart from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bitvavo import Bitvavo diff --git a/freqtrade/exchange/bitmart.py b/freqtrade/exchange/bitmart.py new file mode 100644 index 000000000..5d792b153 --- /dev/null +++ b/freqtrade/exchange/bitmart.py @@ -0,0 +1,20 @@ +""" Bitmart exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bitmart(Exchange): + """ + Bitmart exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + """ + + _ft_has: Dict = { + "stoploss_on_exchange": False, # Bitmart API does not support stoploss orders + "ohlcv_candle_limit": 200, + } diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 10dfdf178..ca986d2be 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -52,7 +52,7 @@ MAP_EXCHANGE_CHILDCLASS = { SUPPORTED_EXCHANGES = [ 'binance', - 'bittrex', + 'bitmart', 'gate', 'huobi', 'kraken', diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7638dcef1..5d0bc704f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -486,11 +486,14 @@ class Exchange: except ccxt.BaseError: logger.exception('Unable to initialize markets.') - def reload_markets(self) -> None: + def reload_markets(self, force: bool = False) -> None: """Reload markets both sync and async if refresh interval has passed """ # Check whether markets have to be reloaded - if (self._last_markets_refresh > 0) and ( - self._last_markets_refresh + self.markets_refresh_interval > dt_ts()): + if ( + not force + and self._last_markets_refresh > 0 + and (self._last_markets_refresh + self.markets_refresh_interval > dt_ts()) + ): return None logger.debug("Performing scheduled market reload..") try: @@ -1228,16 +1231,16 @@ class Exchange: return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {limit_rate}. ' - f'Message: {e}') from e - except ccxt.InvalidOrder as e: + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {limit_rate} with ' + f'stop-price {stop_price_norm}. Message: {e}') from e + except (ccxt.InvalidOrder, ccxt.BadRequest) as e: # Errors: # `Order would trigger immediately.` raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {limit_rate}. ' - f'Message: {e}') from e + f'Could not create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {limit_rate} with ' + f'stop-price {stop_price_norm}. Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: @@ -1496,8 +1499,9 @@ class Exchange: @retrier def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict: """ + :param symbols: List of symbols to fetch :param cached: Allow cached result - :return: fetch_tickers result + :return: fetch_bids_asks result """ if not self.exchange_has('fetchBidsAsks'): return {} @@ -1546,6 +1550,12 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' f'Message: {e}') from e + except ccxt.BadSymbol as e: + logger.warning(f"Could not load tickers due to {e.__class__.__name__}. Message: {e} ." + "Reloading markets.") + self.reload_markets(True) + # Re-raise exception to repeat the call. + raise TemporaryError from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: @@ -1954,7 +1964,7 @@ class Exchange: results = await asyncio.gather(*input_coro, return_exceptions=True) for res in results: - if isinstance(res, Exception): + if isinstance(res, BaseException): logger.warning(f"Async code raised an exception: {repr(res)}") if raise_: raise diff --git a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py index 846d6df2e..b1f2eecc6 100644 --- a/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py +++ b/freqtrade/freqai/prediction_models/PyTorchTransformerRegressor.py @@ -27,6 +27,12 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor): ... "freqai": { ... + "conv_width": 30, // PyTorchTransformer is based on windowing + "feature_parameters": { + ... + "include_shifted_candles": 0, // which removes the need for shifted candles + ... + }, "model_training_parameters" : { "learning_rate": 3e-4, "trainer_kwargs": { @@ -120,16 +126,16 @@ class PyTorchTransformerRegressor(BasePyTorchRegressor): # create empty torch tensor self.model.model.eval() yb = torch.empty(0).to(self.device) - if x.shape[1] > 1: + if x.shape[1] > self.window_size: ws = self.window_size for i in range(0, x.shape[1] - ws): xb = x[:, i:i + ws, :].to(self.device) y = self.model.model(xb) - yb = torch.cat((yb, y), dim=0) + yb = torch.cat((yb, y), dim=1) else: yb = self.model.model(x) - yb = yb.cpu().squeeze() + yb = yb.cpu().squeeze(0) pred_df = pd.DataFrame(yb.detach().numpy(), columns=dk.label_list) pred_df, _, _ = dk.label_pipeline.inverse_transform(pred_df) diff --git a/freqtrade/freqai/tensorboard/TensorboardCallback.py b/freqtrade/freqai/tensorboard/TensorboardCallback.py index 2be917616..b8a351498 100644 --- a/freqtrade/freqai/tensorboard/TensorboardCallback.py +++ b/freqtrade/freqai/tensorboard/TensorboardCallback.py @@ -3,7 +3,6 @@ from typing import Any, Dict, Type, Union from stable_baselines3.common.callbacks import BaseCallback from stable_baselines3.common.logger import HParam -from stable_baselines3.common.vec_env import VecEnv from freqtrade.freqai.RL.BaseEnvironment import BaseActions @@ -13,13 +12,9 @@ class TensorboardCallback(BaseCallback): Custom callback for plotting additional values in tensorboard and episodic summary reports. """ - # Override training_env type to fix type errors - training_env: Union[VecEnv, None] = None - def __init__(self, verbose=1, actions: Type[Enum] = BaseActions): super().__init__(verbose) self.model: Any = None - self.logger: Any = None self.actions: Type[Enum] = actions def _on_training_start(self) -> None: @@ -47,8 +42,6 @@ class TensorboardCallback(BaseCallback): def _on_step(self) -> bool: local_info = self.locals["infos"][0] - if self.training_env is None: - return True if hasattr(self.training_env, 'envs'): tensorboard_metrics = self.training_env.envs[0].unwrapped.tensorboard_metrics diff --git a/freqtrade/misc.py b/freqtrade/misc.py index cbebf99eb..bd4f17d05 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -3,6 +3,7 @@ Various tool function for Freqtrade and scripts """ import gzip import logging +from io import StringIO from pathlib import Path from typing import Any, Dict, Iterator, List, Mapping, Optional, TextIO, Union from urllib.parse import urlparse @@ -231,7 +232,7 @@ def json_to_dataframe(data: str) -> pd.DataFrame: :param data: A JSON string :returns: A pandas DataFrame from the JSON string """ - dataframe = pd.read_json(data, orient='split') + dataframe = pd.read_json(StringIO(data), orient='split') if 'date' in dataframe.columns: dataframe['date'] = pd.to_datetime(dataframe['date'], unit='ms', utc=True) diff --git a/freqtrade/optimize/analysis/lookahead.py b/freqtrade/optimize/analysis/lookahead.py index 924e43e07..9fa4235e7 100755 --- a/freqtrade/optimize/analysis/lookahead.py +++ b/freqtrade/optimize/analysis/lookahead.py @@ -94,8 +94,8 @@ class LookaheadAnalysis(BaseAnalysis): # compare_df now comprises tuples with [1] having either 'self' or 'other' if 'other' in col_name[1]: continue - self_value = compare_df_row[col_idx] - other_value = compare_df_row[col_idx + 1] + self_value = compare_df_row.iloc[col_idx] + other_value = compare_df_row.iloc[col_idx + 1] # output differences if self_value != other_value: diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 13c036a28..a3e2ef058 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) def _format_exception_message(space: str, ignore_missing_space: bool) -> None: msg = (f"The '{space}' space is included into the hyperoptimization " - f"but no parameter for this space was not found in your Strategy. " + f"but no parameter for this space was found in your Strategy. " ) if ignore_missing_space: logger.warning(msg + "This space will be ignored.") diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 6bbf3c15d..ee03aae1a 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -429,14 +429,18 @@ class HyperoptTools: trials = trials.drop(columns=['Total profit']) if print_colorized: + trials2 = trials.astype(str) 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] = f"{Fore.GREEN}{str(trials.loc[i][j])}{Fore.RESET}" + trials2.iat[i, j] = f"{Fore.GREEN}{str(trials.iloc[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] = f"{Style.BRIGHT}{str(trials.loc[i][j])}{Style.RESET_ALL}" - + trials2.iat[i, j] = ( + f"{Style.BRIGHT}{str(trials.iloc[i, j])}{Style.RESET_ALL}" + ) + trials = trials2 + del trials2 trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random']) if remove_header > 0: table = tabulate.tabulate( diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index b68fc708e..2ca6ee947 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -219,8 +219,10 @@ def _get_resample_from_period(period: str) -> str: raise ValueError(f"Period {period} is not supported.") -def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dict[str, Any]]: - results = DataFrame.from_records(trade_list) +def generate_periodic_breakdown_stats( + trade_list: Union[List, DataFrame], period: str) -> List[Dict[str, Any]]: + + results = trade_list if not isinstance(trade_list, list) else DataFrame.from_records(trade_list) if len(results) == 0: return [] results['close_date'] = to_datetime(results['close_date'], utc=True) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index e66c22372..3f721f1e8 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1053,7 +1053,7 @@ class LocalTrade: price = avg_price if is_exit else tmp_price current_stake += price * tmp_amount * side - if current_amount > ZERO: + if current_amount > ZERO and not is_exit: avg_price = current_stake / current_amount if is_exit: @@ -1066,7 +1066,10 @@ class LocalTrade: exit_amount = o.safe_amount_after_fee prof = self.calculate_profit(exit_rate, exit_amount, float(avg_price)) close_profit_abs += prof.profit_abs - close_profit = prof.profit_ratio + if total_stake > 0: + # This needs to be calculated based on the last occuring exit to be aligned + # with realized_profit. + close_profit = (close_profit_abs / total_stake) * self.leverage else: total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price) max_stake_amount += (tmp_amount * price) @@ -1780,7 +1783,7 @@ class Trade(ModelBase, LocalTrade): .order_by(desc('profit_sum_abs')) ).all() - return_list: List[Dict] = [] + resp: List[Dict] = [] for id, enter_tag, exit_reason, profit, profit_abs, count in mix_tag_perf: enter_tag = enter_tag if enter_tag is not None else "Other" exit_reason = exit_reason if exit_reason is not None else "Other" @@ -1788,24 +1791,25 @@ class Trade(ModelBase, LocalTrade): if (exit_reason is not None and enter_tag is not None): mix_tag = enter_tag + " " + exit_reason i = 0 - if not any(item["mix_tag"] == mix_tag for item in return_list): - return_list.append({'mix_tag': mix_tag, - 'profit': profit, - 'profit_pct': round(profit * 100, 2), - 'profit_abs': profit_abs, - 'count': count}) + if not any(item["mix_tag"] == mix_tag for item in resp): + resp.append({'mix_tag': mix_tag, + 'profit_ratio': profit, + 'profit_pct': round(profit * 100, 2), + 'profit_abs': profit_abs, + 'count': count}) else: - while i < len(return_list): - if return_list[i]["mix_tag"] == mix_tag: - return_list[i] = { + while i < len(resp): + if resp[i]["mix_tag"] == mix_tag: + resp[i] = { 'mix_tag': mix_tag, - 'profit': profit + return_list[i]["profit"], - 'profit_pct': round(profit + return_list[i]["profit"] * 100, 2), - 'profit_abs': profit_abs + return_list[i]["profit_abs"], - 'count': 1 + return_list[i]["count"]} + 'profit_ratio': profit + resp[i]["profit_ratio"], + 'profit_pct': round(profit + resp[i]["profit_ratio"] * 100, 2), + 'profit_abs': profit_abs + resp[i]["profit_abs"], + 'count': 1 + resp[i]["count"] + } i += 1 - return return_list + return resp @staticmethod def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)): diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 9ed7bbc46..e0aa2437a 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -21,6 +21,7 @@ from freqtrade.misc import pair_to_filename from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy import IStrategy +from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper logger = logging.getLogger(__name__) @@ -636,7 +637,7 @@ def load_and_plot_trades(config: Config): exchange = ExchangeResolver.load_exchange(config) IStrategy.dp = DataProvider(config, exchange) strategy.ft_bot_start() - strategy.bot_loop_start(datetime.now(timezone.utc)) + strategy_safe_wrapper(strategy.bot_loop_start)(current_time=datetime.now(timezone.utc)) plot_elements = init_plotscript(config, list(exchange.markets), strategy.startup_candle_count) timerange = plot_elements['timerange'] trades = plot_elements['trades'] diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 97f6251bc..7e723bbef 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,9 +1,9 @@ from datetime import date, datetime from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel, ConfigDict, RootModel, SerializeAsAny +from pydantic import BaseModel, RootModel, SerializeAsAny -from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf +from freqtrade.constants import IntOrInf from freqtrade.enums import MarginMode, OrderTypeValues, SignalDirection, TradingMode from freqtrade.types import ValidExchangesType @@ -95,15 +95,30 @@ class Count(BaseModel): total_stake: float -class PerformanceEntry(BaseModel): - pair: str - profit: float +class __BaseStatsModel(BaseModel): profit_ratio: float profit_pct: float profit_abs: float count: int +class Entry(__BaseStatsModel): + enter_tag: str + + +class Exit(__BaseStatsModel): + exit_reason: str + + +class MixTag(__BaseStatsModel): + mix_tag: str + + +class PerformanceEntry(__BaseStatsModel): + pair: str + profit: float + + class Profit(BaseModel): profit_closed_coin: float profit_closed_percent_mean: float @@ -484,11 +499,6 @@ class PairHistory(BaseModel): data_start: str data_stop: str data_stop_ts: int - # TODO[pydantic]: The following keys were removed: `json_encoders`. - # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. - model_config = ConfigDict(json_encoders={ - datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT), - }) class BacktestFreqAIInputs(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 8b1bb2a48..f19010945 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -12,15 +12,15 @@ from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, DailyWeeklyMonthly, - DeleteLockRequest, DeleteTrade, - ExchangeListResponse, ForceEnterPayload, + DeleteLockRequest, DeleteTrade, Entry, + ExchangeListResponse, Exit, ForceEnterPayload, ForceEnterResponse, ForceExitPayload, FreqAIModelListResponse, Health, Locks, Logs, - OpenTradeSchema, PairHistory, PerformanceEntry, - Ping, PlotConfig, Profit, ResultMsg, ShowConfig, - Stats, StatusMsg, StrategyListResponse, - StrategyResponse, SysInfo, Version, - WhitelistResponse) + MixTag, OpenTradeSchema, PairHistory, + PerformanceEntry, Ping, PlotConfig, Profit, + ResultMsg, ShowConfig, Stats, StatusMsg, + StrategyListResponse, StrategyResponse, SysInfo, + Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -52,7 +52,8 @@ logger = logging.getLogger(__name__) # 2.31: new /backtest/history/ delete endpoint # 2.32: new /backtest/history/ patch endpoint # 2.33: Additional weekly/monthly metrics -API_VERSION = 2.33 +# 2.34: new entries/exits/mix_tags endpoints +API_VERSION = 2.34 # Public API, requires no auth. router_public = APIRouter() @@ -83,6 +84,21 @@ def count(rpc: RPC = Depends(get_rpc)): return rpc._rpc_count() +@router.get('/entries', response_model=List[Entry], tags=['info']) +def entries(pair: Optional[str] = None, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_enter_tag_performance(pair) + + +@router.get('/exits', response_model=List[Exit], tags=['info']) +def exits(pair: Optional[str] = None, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_exit_reason_performance(pair) + + +@router.get('/mix_tags', response_model=List[MixTag], tags=['info']) +def mix_tags(pair: Optional[str] = None, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_mix_tag_performance(pair) + + @router.get('/performance', response_model=List[PerformanceEntry], tags=['info']) def performance(rpc: RPC = Depends(get_rpc)): return rpc._rpc_performance() diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index bface89bd..c8c06695a 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -56,7 +56,7 @@ def get_exchange(config=Depends(get_config)): if not (exchange := ApiBG.exchanges.get(exchange_key)): from freqtrade.resolvers import ExchangeResolver exchange = ExchangeResolver.load_exchange( - config, load_leverage_tiers=False) + config, validate=False, load_leverage_tiers=False) ApiBG.exchanges[exchange_key] = exchange return exchange diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 36ef37d01..c0e9220b2 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -31,12 +31,11 @@ class Discord(Webhook): def send_msg(self, msg) -> None: - if msg['type'].value in self._config['discord']: + if (fields := self._config['discord'].get(msg['type'].value)): logger.info(f"Sending discord message: {msg}") msg['strategy'] = self.strategy msg['timeframe'] = self.timeframe - fields = self._config['discord'].get(msg['type'].value) color = 0x0000FF if msg['type'] in (RPCMessageType.EXIT, RPCMessageType.EXIT_FILL): profit_ratio = msg.get('profit_ratio') diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index be27c38f4..c9e9a4733 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -223,7 +223,8 @@ class Telegram(RPCHandler): CommandHandler('health', self._health), CommandHandler('help', self._help), CommandHandler('version', self._version), - CommandHandler('marketdir', self._changemarketdir) + CommandHandler('marketdir', self._changemarketdir), + CommandHandler('order', self._order), ] callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), @@ -240,7 +241,7 @@ class Telegram(RPCHandler): CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"), - CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"), + CallbackQueryHandler(self._force_enter_inline, pattern=r"force_enter__\S+"), ] for handle in handles: self._app.add_handler(handle) @@ -555,6 +556,47 @@ class Telegram(RPCHandler): return lines_detail + @authorized_only + async def _order(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /order. + Returns the orders of the trade + :param bot: telegram bot + :param update: message update + :return: None + """ + + trade_ids = [] + if context.args and len(context.args) > 0: + trade_ids = [int(i) for i in context.args if i.isnumeric()] + + results = self._rpc._rpc_trade_status(trade_ids=trade_ids) + for r in results: + lines = [ + "*Order List for Trade #*`{trade_id}`" + ] + + lines_detail = self._prepare_order_details( + r['orders'], r['quote_currency'], r['is_open']) + lines.extend(lines_detail if lines_detail else "") + await self.__send_order_msg(lines, r) + + async def __send_order_msg(self, lines: List[str], r: Dict[str, Any]) -> None: + """ + Send status message. + """ + msg = '' + + for line in lines: + if line: + if (len(msg) + len(line) + 1) < MAX_MESSAGE_LENGTH: + msg += line + '\n' + else: + await self._send_msg(msg.format(**r)) + msg = "*Order List for Trade #*`{trade_id}` - continued\n" + line + '\n' + + await self._send_msg(msg.format(**r)) + @authorized_only async def _status(self, update: Update, context: CallbackContext) -> None: """ @@ -652,9 +694,6 @@ class Telegram(RPCHandler): "*Open Order:* `{open_orders}`" + ("- `{exit_order_status}`" if r['exit_order_status'] else "")) - lines_detail = self._prepare_order_details( - r['orders'], r['quote_currency'], r['is_open']) - lines.extend(lines_detail if lines_detail else "") await self.__send_status_msg(lines, r) async def __send_status_msg(self, lines: List[str], r: Dict[str, Any]) -> None: @@ -1149,12 +1188,19 @@ class Telegram(RPCHandler): async def _force_enter_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query - if query.data and '_||_' in query.data: - pair, side = query.data.split('_||_') - order_side = SignalDirection(side) - await query.answer() - await query.edit_message_text(text=f"Manually entering {order_side} for {pair}") - await self._force_enter_action(pair, None, order_side) + if query.data and '__' in query.data: + # Input data is "force_enter___" + payload = query.data.split("__")[1] + if payload == 'cancel': + await query.answer() + await query.edit_message_text(text="Force enter canceled.") + return + if payload and '_||_' in payload: + pair, side = payload.split('_||_') + order_side = SignalDirection(side) + await query.answer() + await query.edit_message_text(text=f"Manually entering {order_side} for {pair}") + await self._force_enter_action(pair, None, order_side) @staticmethod def _layout_inline_keyboard( @@ -1183,12 +1229,14 @@ class Telegram(RPCHandler): else: whitelist = self._rpc._rpc_whitelist()['whitelist'] pair_buttons = [ - InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}") - for pair in sorted(whitelist) + InlineKeyboardButton( + text=pair, callback_data=f"force_enter__{pair}_||_{order_side}" + ) for pair in sorted(whitelist) ] buttons_aligned = self._layout_inline_keyboard(pair_buttons) - buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) + buttons_aligned.append([InlineKeyboardButton(text='Cancel', + callback_data='force_enter__cancel')]) await self._send_msg(msg="Which pair?", keyboard=buttons_aligned, query=update.callback_query) @@ -1369,7 +1417,7 @@ class Telegram(RPCHandler): stat_line = ( f"{i+1}.\t {trade['mix_tag']}\t" f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} " - f"({trade['profit']:.2%}) " + f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index 7dfdf5a8c..e83d9433d 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Callable, Optional, Union +from typing import Any, Callable, Dict, Optional, Union from pandas import DataFrame @@ -38,17 +38,18 @@ def informative(timeframe: str, asset: str = '', :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use - current pair. + current pair. Also supports limited pair format strings (see below) :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: * {base}_{quote}_{column}_{timeframe} if asset is specified. * {column}_{timeframe} if asset is not specified. - Format string supports these format variables: - * {asset} - full name of the asset, for example 'BTC/USDT'. + Pair format supports these format variables: * {base} - base currency in lower case, for example 'eth'. * {BASE} - same as {base}, except in upper case. * {quote} - quote currency in lower case, for example 'usdt'. * {QUOTE} - same as {quote}, except in upper case. + Format string additionally supports this variables. + * {asset} - full name of the asset, for example 'BTC/USDT'. * {column} - name of dataframe column. * {timeframe} - timeframe of informative dataframe. :param ffill: ffill dataframe after merging informative pair. @@ -68,9 +69,25 @@ def informative(timeframe: str, asset: str = '', return decorator -def _format_pair_name(config, pair: str) -> str: - return pair.format(stake_currency=config['stake_currency'], - stake=config['stake_currency']).upper() +def __get_pair_formats(market: Optional[Dict[str, Any]]) -> Dict[str, str]: + if not market: + return {} + base = market['base'] + quote = market['quote'] + return { + 'base': base.lower(), + 'BASE': base.upper(), + 'quote': quote.lower(), + 'QUOTE': quote.upper(), + } + + +def _format_pair_name(config, pair: str, market: Optional[Dict[str, Any]] = None) -> str: + return pair.format( + stake_currency=config['stake_currency'], + stake=config['stake_currency'], + **__get_pair_formats(market), + ).upper() def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, @@ -85,7 +102,8 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: if asset: # Insert stake currency if needed. - asset = _format_pair_name(config, asset) + market1 = strategy.dp.market(metadata['pair']) + asset = _format_pair_name(config, asset, market1) else: # Not specifying an asset will define informative dataframe for current pair. asset = metadata['pair'] @@ -93,8 +111,6 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: market = strategy.dp.market(asset) if market is None: raise OperationalException(f'Market {asset} is not available.') - base = market['base'] - quote = market['quote'] # Default format. This optimizes for the common case: informative pairs using same stake # currency. When quote currency matches stake currency, column name will omit base currency. @@ -117,10 +133,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: formatter = fmt.format # A default string formatter. fmt_args = { - 'BASE': base.upper(), - 'QUOTE': quote.upper(), - 'base': base.lower(), - 'quote': quote.lower(), + **__get_pair_formats(market), 'asset': asset, 'timeframe': timeframe, } diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index cbe6afc26..bd846eb90 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -756,12 +756,23 @@ class IStrategy(ABC, HyperStrategyMixin): candle_type = (inf_data.candle_type if inf_data.candle_type else self.config.get('candle_type_def', CandleType.SPOT)) if inf_data.asset: - pair_tf = ( - _format_pair_name(self.config, inf_data.asset), - inf_data.timeframe, - candle_type, - ) - informative_pairs.append(pair_tf) + if any(s in inf_data.asset for s in ("{BASE}", "{base}")): + for pair in self.dp.current_whitelist(): + + pair_tf = ( + _format_pair_name(self.config, inf_data.asset, self.dp.market(pair)), + inf_data.timeframe, + candle_type, + ) + informative_pairs.append(pair_tf) + + else: + pair_tf = ( + _format_pair_name(self.config, inf_data.asset), + inf_data.timeframe, + candle_type, + ) + informative_pairs.append(pair_tf) else: for pair in self.dp.current_whitelist(): informative_pairs.append((pair, inf_data.timeframe, candle_type)) @@ -1006,7 +1017,7 @@ class IStrategy(ABC, HyperStrategyMixin): exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1 exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None) # Tags can be None, which does not resolve to False. - exit_tag = exit_tag if isinstance(exit_tag, str) else None + exit_tag = exit_tag if isinstance(exit_tag, str) and exit_tag != 'nan' else None logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) " f"enter={enter} exit={exit_}") @@ -1038,17 +1049,17 @@ class IStrategy(ABC, HyperStrategyMixin): exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1 enter_signal: Optional[SignalDirection] = None - enter_tag_value: Optional[str] = None + enter_tag: Optional[str] = None if enter_long == 1 and not any([exit_long, enter_short]): enter_signal = SignalDirection.LONG - enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) + enter_tag = latest.get(SignalTagType.ENTER_TAG.value, None) if (self.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT and self.can_short and enter_short == 1 and not any([exit_short, enter_long])): enter_signal = SignalDirection.SHORT - enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) + enter_tag = latest.get(SignalTagType.ENTER_TAG.value, None) - enter_tag_value = enter_tag_value if isinstance(enter_tag_value, str) else None + enter_tag = enter_tag if isinstance(enter_tag, str) and enter_tag != 'nan' else None timeframe_seconds = timeframe_to_seconds(timeframe) @@ -1058,11 +1069,11 @@ class IStrategy(ABC, HyperStrategyMixin): timeframe_seconds=timeframe_seconds, enter=bool(enter_signal) ): - return None, enter_tag_value + return None, enter_tag logger.debug(f"entry trigger: {latest['date']} (pair={pair}) " - f"enter={enter_long} enter_tag_value={enter_tag_value}") - return enter_signal, enter_tag_value + f"enter={enter_long} enter_tag_value={enter_tag}") + return enter_signal, enter_tag def ignore_expired_candle( self, diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index e64570b9e..8be1f0336 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -290,9 +290,6 @@ class FreqaiExampleStrategy(IStrategy): return df - def get_ticker_indicator(self): - return int(self.config["timeframe"][:-1]) - def confirm_trade_entry( self, pair: str, diff --git a/requirements-dev.txt b/requirements-dev.txt index 26c760103..d81e46f29 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,10 +7,10 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.1.1 -mypy==1.6.1 +ruff==0.1.6 +mypy==1.7.1 pre-commit==3.5.0 -pytest==7.4.2 +pytest==7.4.3 pytest-asyncio==0.21.1 pytest-cov==4.1.0 pytest-mock==3.12.0 @@ -20,10 +20,10 @@ isort==5.12.0 time-machine==2.13.0 # Convert jupyter notebooks to markdown documents -nbconvert==7.9.2 +nbconvert==7.11.0 # mypy types -types-cachetools==5.3.0.6 +types-cachetools==5.3.0.7 types-filelock==3.2.7 types-requests==2.31.0.10 types-tabulate==0.9.0.3 diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index c2cca5427..fba25d409 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -5,7 +5,7 @@ torch==2.0.1 #until these branches will be released we can use this gymnasium==0.29.1 -stable_baselines3==2.1.0 +stable_baselines3==2.2.1 sb3_contrib>=2.0.0a9 # Progress bar for stable-baselines3 and sb3-contrib tqdm==4.66.1 diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 67235a1b2..d990b0714 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -7,6 +7,6 @@ scikit-learn==1.1.3 joblib==1.3.2 catboost==1.2.2; 'arm' not in platform_machine lightgbm==4.1.0 -xgboost==2.0.0 -tensorboard==2.15.0 +xgboost==2.0.2 +tensorboard==2.15.1 datasieve==0.1.7 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 33d94ce1b..b4bca9751 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.11.3 +scipy==1.11.4 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.12.4 +filelock==3.13.1 diff --git a/requirements-plot.txt b/requirements-plot.txt index b2ec35539..8900bf1f9 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,4 +1,4 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.17.0 +plotly==5.18.0 diff --git a/requirements.txt b/requirements.txt index 05162ec26..a01dc58ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,46 +1,44 @@ -numpy==1.25.2; platform_machine == 'armv7l' -numpy==1.26.1; platform_machine != 'armv7l' -pandas==2.0.3 +numpy==1.26.2 +pandas==2.1.3 pandas-ta==0.3.14b -ccxt==4.1.22 -cryptography==41.0.4 -aiohttp==3.8.6 -SQLAlchemy==2.0.22 +ccxt==4.1.66 +cryptography==41.0.7 +aiohttp==3.9.1 +SQLAlchemy==2.0.23 python-telegram-bot==20.6 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 arrow==1.3.0 -cachetools==5.3.1 +cachetools==5.3.2 requests==2.31.0 -urllib3==2.0.7 -jsonschema==4.19.1 +urllib3==2.1.0 +jsonschema==4.20.0 TA-Lib==0.4.28 technical==1.4.0 tabulate==0.9.0 pycoingecko==3.1.0 jinja2==3.1.2 -tables==3.8.0 -blosc==1.11.1 +tables==3.9.1 joblib==1.3.2 -rich==13.6.0 -pyarrow==13.0.0; platform_machine != 'armv7l' +rich==13.7.0 +pyarrow==14.0.1; platform_machine != 'armv7l' # find first, C search in arrays -py_find_1st==1.1.5 +py_find_1st==1.1.6 # Load ticker files 30% faster -python-rapidjson==1.12 +python-rapidjson==1.13 # Properly format api responses -orjson==3.9.9 +orjson==3.9.10 # Notify systemd sdnotify==0.3.2 # API Server -fastapi==0.104.0 -pydantic==2.4.2 -uvicorn==0.23.2 +fastapi==0.104.1 +pydantic==2.5.2 +uvicorn==0.24.0.post1 pyjwt==2.8.0 aiofiles==23.2.1 psutil==5.9.6 @@ -60,5 +58,5 @@ schedule==1.2.1 websockets==12.0 janus==1.0.0 -ast-comments==1.1.2 +ast-comments==1.2.0 packaging==23.2 diff --git a/scripts/rest_client.py b/scripts/rest_client.py index dfe50cc2c..5970b0c5b 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -112,6 +112,30 @@ class FtRestClient: """ return self._get("count") + def entries(self, pair=None): + """Returns List of dicts containing all Trades, based on buy tag performance + Can either be average for all pairs or a specific pair provided + + :return: json object + """ + return self._get("entries", params={"pair": pair} if pair else None) + + def exits(self, pair=None): + """Returns List of dicts containing all Trades, based on exit reason performance + Can either be average for all pairs or a specific pair provided + + :return: json object + """ + return self._get("exits", params={"pair": pair} if pair else None) + + def mix_tags(self, pair=None): + """Returns List of dicts containing all Trades, based on entry_tag + exit_reason performance + Can either be average for all pairs or a specific pair provided + + :return: json object + """ + return self._get("mix_tags", params={"pair": pair} if pair else None) + def locks(self): """Return current locks diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index e066eac01..7698740a5 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -550,7 +550,7 @@ def test_start_install_ui(mocker): assert download_mock.call_count == 0 -def test_clean_ui_subdir(mocker, tmpdir, caplog): +def test_clean_ui_subdir(mocker, tmp_path, caplog): mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", side_effect=[True, True]) mocker.patch("freqtrade.commands.deploy_commands.Path.is_file", @@ -560,14 +560,14 @@ def test_clean_ui_subdir(mocker, tmpdir, caplog): mocker.patch("freqtrade.commands.deploy_commands.Path.glob", return_value=[Path('test1'), Path('test2'), Path('.gitkeep')]) - folder = Path(tmpdir) / "uitests" + folder = tmp_path / "uitests" clean_ui_subdir(folder) assert log_has("Removing UI directory content.", caplog) assert rd_mock.call_count == 1 assert ul_mock.call_count == 1 -def test_download_and_install_ui(mocker, tmpdir): +def test_download_and_install_ui(mocker, tmp_path): # Create zipfile requests_mock = MagicMock() file_like_object = BytesIO() @@ -583,7 +583,7 @@ def test_download_and_install_ui(mocker, tmpdir): side_effect=[True, False]) wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") - folder = Path(tmpdir) / "uitests_dl" + folder = tmp_path / "uitests_dl" folder.mkdir(exist_ok=True) assert read_ui_version(folder) is None @@ -1010,8 +1010,8 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): pytest.fail(f'Expected well formed JSON, but failed to parse: {captured.out}') -def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir): - csv_file = Path(tmpdir) / "test.csv" +def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmp_path): + csv_file = tmp_path / "test.csv" mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist', return_value=True @@ -1512,10 +1512,10 @@ def test_backtesting_show(mocker, testdatadir, capsys): assert "Pairs for Strategy" in out -def test_start_convert_db(mocker, fee, tmpdir, caplog): - db_src_file = Path(f"{tmpdir}/db.sqlite") +def test_start_convert_db(fee, tmp_path): + db_src_file = tmp_path / "db.sqlite" db_from = f"sqlite:///{db_src_file}" - db_target_file = Path(f"{tmpdir}/db_target.sqlite") + db_target_file = tmp_path / "db_target.sqlite" db_to = f"sqlite:///{db_target_file}" args = [ "convert-db", @@ -1542,13 +1542,13 @@ def test_start_convert_db(mocker, fee, tmpdir, caplog): assert db_target_file.is_file() -def test_start_strategy_updater(mocker, tmpdir): +def test_start_strategy_updater(mocker, tmp_path): sc_mock = mocker.patch('freqtrade.commands.strategy_utils_commands.start_conversion') teststrats = Path(__file__).parent.parent / 'strategy/strats' args = [ "strategy-updater", "--userdir", - str(tmpdir), + str(tmp_path), "--strategy-path", str(teststrats), ] @@ -1562,7 +1562,7 @@ def test_start_strategy_updater(mocker, tmpdir): args = [ "strategy-updater", "--userdir", - str(tmpdir), + str(tmp_path), "--strategy-path", str(teststrats), "--strategy-list", diff --git a/tests/conftest.py b/tests/conftest.py index 4372534ce..b18032621 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -413,8 +413,8 @@ def patch_gc(mocker) -> None: @pytest.fixture(autouse=True) -def user_dir(mocker, tmpdir) -> Path: - user_dir = Path(tmpdir) / "user_data" +def user_dir(mocker, tmp_path) -> Path: + user_dir = tmp_path / "user_data" mocker.patch('freqtrade.configuration.configuration.create_userdata_dir', return_value=user_dir) return user_dir diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 74bb8d9f9..99c56e1d0 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 import logging -from pathlib import Path from shutil import copyfile import numpy as np @@ -50,8 +49,8 @@ def test_trades_to_ohlcv(trades_history_df, caplog): assert 'high' in df.columns assert 'low' in df.columns assert 'close' in df.columns - assert df.loc[:, 'high'][0] == 0.019627 - assert df.loc[:, 'low'][0] == 0.019626 + assert df.iloc[0, :]['high'] == 0.019627 + assert df.iloc[0, :]['low'] == 0.019626 def test_ohlcv_fill_up_missing_data(testdatadir, caplog): @@ -323,18 +322,17 @@ def test_trades_dict_to_list(fetch_trades_result): assert t[6] == fetch_trades_result[i]['cost'] -def test_convert_trades_format(default_conf, testdatadir, tmpdir): - tmpdir1 = Path(tmpdir) - files = [{'old': tmpdir1 / "XRP_ETH-trades.json.gz", - 'new': tmpdir1 / "XRP_ETH-trades.json"}, - {'old': tmpdir1 / "XRP_OLD-trades.json.gz", - 'new': tmpdir1 / "XRP_OLD-trades.json"}, +def test_convert_trades_format(default_conf, testdatadir, tmp_path): + files = [{'old': tmp_path / "XRP_ETH-trades.json.gz", + 'new': tmp_path / "XRP_ETH-trades.json"}, + {'old': tmp_path / "XRP_OLD-trades.json.gz", + 'new': tmp_path / "XRP_OLD-trades.json"}, ] for file in files: copyfile(testdatadir / file['old'].name, file['old']) assert not file['new'].exists() - default_conf['datadir'] = tmpdir1 + default_conf['datadir'] = tmp_path convert_trades_format(default_conf, convert_from='jsongz', convert_to='json', erase=False) @@ -362,16 +360,15 @@ def test_convert_trades_format(default_conf, testdatadir, tmpdir): (['UNITTEST_USDT_USDT-1h-mark', 'XRP_USDT_USDT-1h-mark'], CandleType.MARK), (['XRP_USDT_USDT-1h-futures'], CandleType.FUTURES), ]) -def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, candletype): - tmpdir1 = Path(tmpdir) +def test_convert_ohlcv_format(default_conf, testdatadir, tmp_path, file_base, candletype): prependix = '' if candletype == CandleType.SPOT else 'futures/' files_orig = [] files_temp = [] files_new = [] for file in file_base: file_orig = testdatadir / f"{prependix}{file}.feather" - file_temp = tmpdir1 / f"{prependix}{file}.feather" - file_new = tmpdir1 / f"{prependix}{file}.json.gz" + file_temp = tmp_path / f"{prependix}{file}.feather" + file_new = tmp_path / f"{prependix}{file}.json.gz" IDataHandler.create_dir_if_needed(file_temp) copyfile(file_orig, file_temp) @@ -379,7 +376,7 @@ def test_convert_ohlcv_format(default_conf, testdatadir, tmpdir, file_base, cand files_temp.append(file_temp) files_new.append(file_new) - default_conf['datadir'] = tmpdir1 + default_conf['datadir'] = tmp_path default_conf['candle_types'] = [candletype] if candletype == CandleType.SPOT: @@ -445,30 +442,29 @@ def test_reduce_dataframe_footprint(): assert df2['close_copy'].dtype == np.float32 -def test_convert_trades_to_ohlcv(testdatadir, tmpdir, caplog): - tmpdir1 = Path(tmpdir) +def test_convert_trades_to_ohlcv(testdatadir, tmp_path, caplog): pair = 'XRP/ETH' - file1 = tmpdir1 / 'XRP_ETH-1m.feather' - file5 = tmpdir1 / 'XRP_ETH-5m.feather' - filetrades = tmpdir1 / 'XRP_ETH-trades.json.gz' + file1 = tmp_path / 'XRP_ETH-1m.feather' + file5 = tmp_path / 'XRP_ETH-5m.feather' + filetrades = tmp_path / 'XRP_ETH-trades.json.gz' copyfile(testdatadir / file1.name, file1) copyfile(testdatadir / file5.name, file5) copyfile(testdatadir / filetrades.name, filetrades) # Compare downloaded dataset with converted dataset - dfbak_1m = load_pair_history(datadir=tmpdir1, timeframe="1m", pair=pair) - dfbak_5m = load_pair_history(datadir=tmpdir1, timeframe="5m", pair=pair) + dfbak_1m = load_pair_history(datadir=tmp_path, timeframe="1m", pair=pair) + dfbak_5m = load_pair_history(datadir=tmp_path, timeframe="5m", pair=pair) tr = TimeRange.parse_timerange('20191011-20191012') convert_trades_to_ohlcv([pair], timeframes=['1m', '5m'], data_format_trades='jsongz', - datadir=tmpdir1, timerange=tr, erase=True) + datadir=tmp_path, timerange=tr, erase=True) assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) # Load new data - df_1m = load_pair_history(datadir=tmpdir1, timeframe="1m", pair=pair) - df_5m = load_pair_history(datadir=tmpdir1, timeframe="5m", pair=pair) + df_1m = load_pair_history(datadir=tmp_path, timeframe="1m", pair=pair) + df_5m = load_pair_history(datadir=tmp_path, timeframe="5m", pair=pair) assert_frame_equal(dfbak_1m, df_1m, check_exact=True) assert_frame_equal(dfbak_5m, df_5m, check_exact=True) @@ -477,5 +473,5 @@ def test_convert_trades_to_ohlcv(testdatadir, tmpdir, caplog): convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'], data_format_trades='jsongz', - datadir=tmpdir1, timerange=tr, erase=True) + datadir=tmp_path, timerange=tr, erase=True) assert log_has(msg, caplog) diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 8415bea7c..988e7ea55 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -328,17 +328,16 @@ def test_hdf5datahandler_trades_load(testdatadir): ]) def test_hdf5datahandler_ohlcv_load_and_resave( testdatadir, - tmpdir, + tmp_path, pair, timeframe, candle_type, candle_append, startdt, enddt ): - tmpdir1 = Path(tmpdir) - tmpdir2 = tmpdir1 + tmpdir2 = tmp_path if candle_type not in ('', 'spot'): - tmpdir2 = tmpdir1 / 'futures' + tmpdir2 = tmp_path / 'futures' tmpdir2.mkdir() dh = get_datahandler(testdatadir, 'hdf5') ohlcv = dh._ohlcv_load(pair, timeframe, None, candle_type=candle_type) @@ -348,7 +347,7 @@ def test_hdf5datahandler_ohlcv_load_and_resave( file = tmpdir2 / f"UNITTEST_NEW-{timeframe}{candle_append}.h5" assert not file.is_file() - dh1 = get_datahandler(tmpdir1, 'hdf5') + dh1 = get_datahandler(tmp_path, 'hdf5') dh1.ohlcv_store('UNITTEST/NEW', timeframe, ohlcv, candle_type=candle_type) assert file.is_file() @@ -379,17 +378,16 @@ def test_hdf5datahandler_ohlcv_load_and_resave( def test_generic_datahandler_ohlcv_load_and_resave( datahandler, testdatadir, - tmpdir, + tmp_path, pair, timeframe, candle_type, candle_append, startdt, enddt ): - tmpdir1 = Path(tmpdir) - tmpdir2 = tmpdir1 + tmpdir2 = tmp_path if candle_type not in ('', 'spot'): - tmpdir2 = tmpdir1 / 'futures' + tmpdir2 = tmp_path / 'futures' tmpdir2.mkdir() # Load data from one common file dhbase = get_datahandler(testdatadir, 'feather') @@ -403,7 +401,7 @@ def test_generic_datahandler_ohlcv_load_and_resave( file = tmpdir2 / f"UNITTEST_NEW-{timeframe}{candle_append}.{dh._get_file_extension()}" assert not file.is_file() - dh1 = get_datahandler(tmpdir1, datahandler) + dh1 = get_datahandler(tmp_path, datahandler) dh1.ohlcv_store('UNITTEST/NEW', timeframe, ohlcv, candle_type=candle_type) assert file.is_file() @@ -459,15 +457,14 @@ def test_datahandler_trades_load(testdatadir, datahandler): @pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet']) -def test_datahandler_trades_store(testdatadir, tmpdir, datahandler): - tmpdir1 = Path(tmpdir) +def test_datahandler_trades_store(testdatadir, tmp_path, datahandler): dh = get_datahandler(testdatadir, datahandler) trades = dh.trades_load('XRP/ETH') - dh1 = get_datahandler(tmpdir1, datahandler) + dh1 = get_datahandler(tmp_path, datahandler) dh1.trades_store('XRP/NEW', trades) - file = tmpdir1 / f'XRP_NEW-trades.{dh1._get_file_extension()}' + file = tmp_path / f'XRP_NEW-trades.{dh1._get_file_extension()}' assert file.is_file() # Load trades back trades_new = dh1.trades_load('XRP/NEW') diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 13fdb034b..100916387 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -106,17 +106,16 @@ def test_load_data_startup_candles(mocker, testdatadir) -> None: @pytest.mark.parametrize('candle_type', ['mark', '']) def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, - default_conf, tmpdir, candle_type) -> None: + default_conf, tmp_path, candle_type) -> None: """ Test load_pair_history() with 1 min timeframe """ - tmpdir1 = Path(tmpdir) mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) - file = tmpdir1 / 'MEME_BTC-1m.feather' + file = tmp_path / 'MEME_BTC-1m.feather' # do not download a new pair if refresh_pairs isn't set - load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC', candle_type=candle_type) + load_pair_history(datadir=tmp_path, timeframe='1m', pair='MEME/BTC', candle_type=candle_type) assert not file.is_file() assert log_has( f"No history for MEME/BTC, {candle_type}, 1m found. " @@ -124,10 +123,10 @@ def test_load_data_with_new_pair_1min(ohlcv_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - refresh_data(datadir=tmpdir1, timeframe='1m', pairs=['MEME/BTC'], + refresh_data(datadir=tmp_path, timeframe='1m', pairs=['MEME/BTC'], exchange=exchange, candle_type=CandleType.SPOT ) - load_pair_history(datadir=tmpdir1, timeframe='1m', pair='MEME/BTC', candle_type=candle_type) + load_pair_history(datadir=tmp_path, timeframe='1m', pair='MEME/BTC', candle_type=candle_type) assert file.is_file() assert log_has_re( r'\(0/1\) - Download history data for "MEME/BTC", 1m, ' @@ -273,27 +272,26 @@ def test_download_pair_history( ohlcv_history_list, mocker, default_conf, - tmpdir, + tmp_path, candle_type, subdir, file_tail ) -> None: mocker.patch(f'{EXMS}.get_historic_ohlcv', return_value=ohlcv_history_list) exchange = get_patched_exchange(mocker, default_conf) - tmpdir1 = Path(tmpdir) - file1_1 = tmpdir1 / f'{subdir}MEME_BTC-1m{file_tail}.feather' - file1_5 = tmpdir1 / f'{subdir}MEME_BTC-5m{file_tail}.feather' - file2_1 = tmpdir1 / f'{subdir}CFI_BTC-1m{file_tail}.feather' - file2_5 = tmpdir1 / f'{subdir}CFI_BTC-5m{file_tail}.feather' + file1_1 = tmp_path / f'{subdir}MEME_BTC-1m{file_tail}.feather' + file1_5 = tmp_path / f'{subdir}MEME_BTC-5m{file_tail}.feather' + file2_1 = tmp_path / f'{subdir}CFI_BTC-1m{file_tail}.feather' + file2_5 = tmp_path / f'{subdir}CFI_BTC-5m{file_tail}.feather' assert not file1_1.is_file() assert not file2_1.is_file() - assert _download_pair_history(datadir=tmpdir1, exchange=exchange, + assert _download_pair_history(datadir=tmp_path, exchange=exchange, pair='MEME/BTC', timeframe='1m', candle_type=candle_type) - assert _download_pair_history(datadir=tmpdir1, exchange=exchange, + assert _download_pair_history(datadir=tmp_path, exchange=exchange, pair='CFI/BTC', timeframe='1m', candle_type=candle_type) @@ -308,11 +306,11 @@ def test_download_pair_history( assert not file1_5.is_file() assert not file2_5.is_file() - assert _download_pair_history(datadir=tmpdir1, exchange=exchange, + assert _download_pair_history(datadir=tmp_path, exchange=exchange, pair='MEME/BTC', timeframe='5m', candle_type=candle_type) - assert _download_pair_history(datadir=tmpdir1, exchange=exchange, + assert _download_pair_history(datadir=tmp_path, exchange=exchange, pair='CFI/BTC', timeframe='5m', candle_type=candle_type) @@ -340,13 +338,12 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: assert json_dump_mock.call_count == 3 -def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmpdir) -> None: +def test_download_backtesting_data_exception(mocker, caplog, default_conf, tmp_path) -> None: mocker.patch(f'{EXMS}.get_historic_ohlcv', side_effect=Exception('File Error')) - tmpdir1 = Path(tmpdir) exchange = get_patched_exchange(mocker, default_conf) - assert not _download_pair_history(datadir=tmpdir1, exchange=exchange, + assert not _download_pair_history(datadir=tmp_path, exchange=exchange, pair='MEME/BTC', timeframe='1m', candle_type='spot') assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog) @@ -570,16 +567,15 @@ def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, tes def test_download_trades_history(trades_history, mocker, default_conf, testdatadir, caplog, - tmpdir, time_machine) -> None: + tmp_path, time_machine) -> None: start_dt = dt_utc(2023, 1, 1) time_machine.move_to(start_dt, tick=False) - tmpdir1 = Path(tmpdir) ght_mock = MagicMock(side_effect=lambda pair, *args, **kwargs: (pair, trades_history)) mocker.patch(f'{EXMS}.get_historic_trades', ght_mock) exchange = get_patched_exchange(mocker, default_conf) - file1 = tmpdir1 / 'ETH_BTC-trades.json.gz' - data_handler = get_datahandler(tmpdir1, data_format='jsongz') + file1 = tmp_path / 'ETH_BTC-trades.json.gz' + data_handler = get_datahandler(tmp_path, data_format='jsongz') assert not file1.is_file() @@ -614,7 +610,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad pair='ETH/BTC') assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) - file2 = tmpdir1 / 'XRP_ETH-trades.json.gz' + file2 = tmp_path / 'XRP_ETH-trades.json.gz' copyfile(testdatadir / file2.name, file2) ght_mock.reset_mock() diff --git a/tests/data/test_trade_converter_kraken.py b/tests/data/test_trade_converter_kraken.py index 2c5bf71e0..bb44062bf 100644 --- a/tests/data/test_trade_converter_kraken.py +++ b/tests/data/test_trade_converter_kraken.py @@ -1,5 +1,4 @@ from datetime import datetime, timezone -from pathlib import Path from shutil import copytree from unittest.mock import PropertyMock @@ -11,7 +10,7 @@ from freqtrade.exceptions import OperationalException from tests.conftest import EXMS, log_has, log_has_re, patch_exchange -def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf_usdt, mocker): +def test_import_kraken_trades_from_csv(testdatadir, tmp_path, caplog, default_conf_usdt, mocker): with pytest.raises(OperationalException, match="This function is only for the kraken exchange"): import_kraken_trades_from_csv(default_conf_usdt, 'feather') @@ -21,10 +20,9 @@ def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={ 'BCH/EUR': {'symbol': 'BCH/EUR', 'id': 'BCHEUR', 'altname': 'BCHEUR'}, })) - tmpdir1 = Path(tmpdir) - dstfile = tmpdir1 / 'BCH_EUR-trades.feather' + dstfile = tmp_path / 'BCH_EUR-trades.feather' assert not dstfile.is_file() - default_conf_usdt['datadir'] = tmpdir1 + default_conf_usdt['datadir'] = tmp_path # There's 2 files in this tree, containing a total of 2 days. # tests/testdata/kraken/ # └── trades_csv @@ -32,7 +30,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf # └── incremental_q2 # └── BCHEUR.csv <-- 2023-01-02 - copytree(testdatadir / 'kraken/trades_csv', tmpdir1 / 'trades_csv') + copytree(testdatadir / 'kraken/trades_csv', tmp_path / 'trades_csv') import_kraken_trades_from_csv(default_conf_usdt, 'feather') assert log_has("Found csv files for BCHEUR.", caplog) @@ -40,7 +38,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmpdir, caplog, default_conf assert dstfile.is_file() - dh = get_datahandler(tmpdir1, 'feather') + dh = get_datahandler(tmp_path, 'feather') trades = dh.trades_load('BCH_EUR') assert len(trades) == 340 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e2b3cc102..ad993cb6d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1851,7 +1851,7 @@ def test_fetch_bids_asks(default_conf, mocker): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_tickers(default_conf, mocker, exchange_name): +def test_get_tickers(default_conf, mocker, exchange_name, caplog): api_mock = MagicMock() tick = {'ETH/BTC': { 'symbol': 'ETH/BTC', @@ -1900,6 +1900,14 @@ def test_get_tickers(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() + caplog.clear() + api_mock.fetch_tickers = MagicMock(side_effect=[ccxt.BadSymbol("SomeSymbol"), []]) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + x = exchange.get_tickers() + assert x == [] + assert log_has_re(r'Could not load tickers due to BadSymbol\..*SomeSymbol', caplog) + caplog.clear() + api_mock.fetch_tickers = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() diff --git a/tests/exchange/test_exchange_utils.py b/tests/exchange/test_exchange_utils.py index 377514468..6a4137369 100644 --- a/tests/exchange/test_exchange_utils.py +++ b/tests/exchange/test_exchange_utils.py @@ -18,7 +18,7 @@ from tests.conftest import log_has_re def test_check_exchange(default_conf, caplog) -> None: # Test an officially supported by Freqtrade team exchange default_conf['runmode'] = RunMode.DRY_RUN - default_conf.get('exchange').update({'name': 'BITTREX'}) + default_conf.get('exchange').update({'name': 'BINANCE'}) assert check_exchange(default_conf) assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", caplog) @@ -41,14 +41,14 @@ def test_check_exchange(default_conf, caplog) -> None: caplog.clear() # Test an officially supported by Freqtrade team exchange - with remapping - default_conf.get('exchange').update({'name': 'okex'}) + default_conf.get('exchange').update({'name': 'okx'}) assert check_exchange(default_conf) assert log_has_re( - r"Exchange \"okex\" is officially supported by the Freqtrade development team\.", + r"Exchange \"okx\" is officially supported by the Freqtrade development team\.", caplog) caplog.clear() # Test an available exchange, supported by ccxt - default_conf.get('exchange').update({'name': 'huobipro'}) + default_conf.get('exchange').update({'name': 'huobijp'}) assert check_exchange(default_conf) assert log_has_re(r"Exchange .* is known to the the ccxt library, available for the bot, " r"but not officially supported " diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 736c630e0..fe9ab3c18 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -1,5 +1,4 @@ from datetime import datetime, timedelta, timezone -from pathlib import Path from unittest.mock import AsyncMock, MagicMock, PropertyMock import ccxt @@ -269,9 +268,9 @@ def test_additional_exchange_init_okx(default_conf, mocker): "additional_exchange_init", "fetch_accounts") -def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, time_machine): +def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmp_path, caplog, time_machine): - default_conf['datadir'] = Path(tmpdir) + default_conf['datadir'] = tmp_path # fd_mock = mocker.patch('freqtrade.exchange.exchange.file_dump_json') api_mock = MagicMock() type(api_mock).has = PropertyMock(return_value={ diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index c5f59ee0e..35c9a9d85 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -227,6 +227,7 @@ EXCHANGES = { 'timeframe': '1h', 'futures_pair': 'BTC/USDT:USDT', 'futures': True, + 'orderbook_max_entries': 50, 'leverage_tiers_public': True, 'leverage_in_spot_market': True, 'sample_order': [ @@ -247,6 +248,13 @@ EXCHANGES = { } ] }, + 'bitmart': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '1h', + 'orderbook_max_entries': 50, + }, 'huobi': { 'pair': 'ETH/BTC', 'stake_currency': 'BTC', diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index aa3dfdfae..b48d70de2 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -133,6 +133,7 @@ class TestCCXTExchange: exch, exchangename = exchange pair = EXCHANGES[exchangename]['pair'] l2 = exch.fetch_l2_order_book(pair) + orderbook_max_entries = EXCHANGES[exchangename].get('orderbook_max_entries') assert 'asks' in l2 assert 'bids' in l2 assert len(l2['asks']) >= 1 @@ -143,7 +144,7 @@ class TestCCXTExchange: # TODO: Gate is unstable here at the moment, ignoring the limit partially. return for val in [1, 2, 5, 25, 50, 100]: - if val > 50 and exchangename == 'bybit': + if orderbook_max_entries and val > orderbook_max_entries: continue l2 = exch.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 9c7a950e7..be208408f 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -21,13 +21,13 @@ def is_mac() -> bool: @pytest.fixture(scope="function") -def freqai_conf(default_conf, tmpdir): +def freqai_conf(default_conf, tmp_path): freqaiconf = deepcopy(default_conf) freqaiconf.update( { "datadir": Path(default_conf["datadir"]), "strategy": "freqai_test_strat", - "user_data_dir": Path(tmpdir), + "user_data_dir": tmp_path, "strategy-path": "freqtrade/tests/strategy/strats", "freqaimodel": "LightGBMRegressor", "freqaimodel_path": "freqai/prediction_models", diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 55338f611..7638c03ed 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -500,14 +500,14 @@ def test_get_required_data_timerange(mocker, freqai_conf): assert (time_range.stopts - time_range.startts) == 177300 -def test_download_all_data_for_training(mocker, freqai_conf, caplog, tmpdir): +def test_download_all_data_for_training(mocker, freqai_conf, caplog, tmp_path): caplog.set_level(logging.DEBUG) strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) pairlist = PairListManager(exchange, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange, pairlist) freqai_conf['pairs'] = freqai_conf['exchange']['pair_whitelist'] - freqai_conf['datadir'] = Path(tmpdir) + freqai_conf['datadir'] = tmp_path download_all_data_for_training(strategy.dp, freqai_conf) assert log_has_re( diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index ed5eeafd6..67f776c7e 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -193,8 +193,8 @@ def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> None: start_hyperopt(pargs) -def test_start_no_data(mocker, hyperopt_conf, tmpdir) -> None: - hyperopt_conf['user_data_dir'] = Path(tmpdir) +def test_start_no_data(mocker, hyperopt_conf, tmp_path) -> None: + hyperopt_conf['user_data_dir'] = tmp_path patched_configuration_load_config_file(mocker, hyperopt_conf) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame)) mocker.patch( @@ -310,6 +310,8 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) + # Dummy-reduce points to ensure scikit-learn is forced to generate new values + mocker.patch('freqtrade.optimize.hyperopt.INITIAL_POINTS', 2) parallel = mocker.patch( 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', @@ -857,14 +859,16 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None: hyperopt.start() -def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: +def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None: patch_exchange(mocker) mocker.patch(f'{EXMS}.get_fee', fee) - (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + # Dummy-reduce points to ensure scikit-learn is forced to generate new values + mocker.patch('freqtrade.optimize.hyperopt.INITIAL_POINTS', 2) + (tmp_path / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', - 'user_data_dir': Path(tmpdir), + 'user_data_dir': tmp_path, 'hyperopt_random_state': 42, 'spaces': ['all'], }) @@ -897,17 +901,19 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: hyperopt.get_optimizer([], 2) -def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None: +def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None: mocker.patch(f'{EXMS}.validate_config', MagicMock()) mocker.patch(f'{EXMS}.get_fee', fee) mocker.patch(f'{EXMS}._load_markets') mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=get_markets())) - (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + (tmp_path / 'hyperopt_results').mkdir(parents=True) + # Dummy-reduce points to ensure scikit-learn is forced to generate new values + mocker.patch('freqtrade.optimize.hyperopt.INITIAL_POINTS', 2) # No hyperopt needed hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', - 'user_data_dir': Path(tmpdir), + 'user_data_dir': tmp_path, 'hyperopt_random_state': 42, 'spaces': ['all'], # Enforce parallelity @@ -938,14 +944,14 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, hyperopt.start() -def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmpdir, fee) -> None: +def test_in_strategy_auto_hyperopt_per_epoch(mocker, hyperopt_conf, tmp_path, fee) -> None: patch_exchange(mocker) mocker.patch(f'{EXMS}.get_fee', fee) - (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + (tmp_path / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', - 'user_data_dir': Path(tmpdir), + 'user_data_dir': tmp_path, 'hyperopt_random_state': 42, 'spaces': ['all'], 'epochs': 3, @@ -995,15 +1001,15 @@ def test_SKDecimal(): assert space.transform([1.5, 1.6]) == [150, 160] -def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, fee) -> None: +def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmp_path, fee) -> None: # This test is to ensure that unlimited max_open_trades are ignored for the backtesting # if we have an unlimited stake amount patch_exchange(mocker) mocker.patch(f'{EXMS}.get_fee', fee) - (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + (tmp_path / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', - 'user_data_dir': Path(tmpdir), + 'user_data_dir': tmp_path, 'hyperopt_random_state': 42, 'spaces': ['trades'], 'stake_amount': 'unlimited' @@ -1023,15 +1029,15 @@ def test_stake_amount_unlimited_max_open_trades(mocker, hyperopt_conf, tmpdir, f assert hyperopt.backtesting.strategy.max_open_trades == 1 -def test_max_open_trades_dump(mocker, hyperopt_conf, tmpdir, fee, capsys) -> None: +def test_max_open_trades_dump(mocker, hyperopt_conf, tmp_path, fee, capsys) -> None: # This test is to ensure that after hyperopting, max_open_trades is never # saved as inf in the output json params patch_exchange(mocker) mocker.patch(f'{EXMS}.get_fee', fee) - (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + (tmp_path / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', - 'user_data_dir': Path(tmpdir), + 'user_data_dir': tmp_path, 'hyperopt_random_state': 42, 'spaces': ['trades'], }) @@ -1069,16 +1075,16 @@ def test_max_open_trades_dump(mocker, hyperopt_conf, tmpdir, fee, capsys) -> Non assert '"max_open_trades":-1' in out -def test_max_open_trades_consistency(mocker, hyperopt_conf, tmpdir, fee) -> None: +def test_max_open_trades_consistency(mocker, hyperopt_conf, tmp_path, fee) -> None: # This test is to ensure that max_open_trades is the same across all functions needing it # after it has been changed from the hyperopt patch_exchange(mocker) mocker.patch(f'{EXMS}.get_fee', return_value=0) - (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + (tmp_path / 'hyperopt_results').mkdir(parents=True) hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', - 'user_data_dir': Path(tmpdir), + 'user_data_dir': tmp_path, 'hyperopt_random_state': 42, 'spaces': ['trades'], 'stake_amount': 'unlimited', diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index eace78eee..c46897374 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -19,9 +19,9 @@ def create_results() -> List[Dict]: return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}] -def test_save_results_saves_epochs(hyperopt, tmpdir, caplog) -> None: +def test_save_results_saves_epochs(hyperopt, tmp_path, caplog) -> None: - hyperopt.results_file = Path(tmpdir / 'ut_results.fthypt') + hyperopt.results_file = tmp_path / 'ut_results.fthypt' hyperopt_epochs = HyperoptTools.load_filtered_results(hyperopt.results_file, {}) assert log_has_re("Hyperopt file .* not found.", caplog) @@ -182,9 +182,9 @@ def test_get_strategy_filename(default_conf): assert x is None -def test_export_params(tmpdir): +def test_export_params(tmp_path): - filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json" + filename = tmp_path / f"{CURRENT_TEST_STRATEGY}.json" assert not filename.is_file() params = { "params_details": { @@ -231,11 +231,11 @@ def test_export_params(tmpdir): assert "max_open_trades" in content["params"] -def test_try_export_params(default_conf, tmpdir, caplog, mocker): +def test_try_export_params(default_conf, tmp_path, caplog, mocker): default_conf['disableparamexport'] = False export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params") - filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json" + filename = tmp_path / f"{CURRENT_TEST_STRATEGY}.json" assert not filename.is_file() params = { "params_details": { diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 7624757e2..a65c3f277 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -74,7 +74,7 @@ def test_text_table_bt_results(): assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str -def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): +def test_generate_backtest_stats(default_conf, testdatadir, tmp_path): default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) StrategyResolver.load_strategy(default_conf) @@ -185,8 +185,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): assert strat_stats['pairlist'] == ['UNITTEST/BTC'] # Test storing stats - filename = Path(tmpdir / 'btresult.json') - filename_last = Path(tmpdir / LAST_BT_RESULT_FN) + filename = tmp_path / 'btresult.json' + filename_last = tmp_path / LAST_BT_RESULT_FN _backup_file(filename_last, copy_file=True) assert not filename.is_file() @@ -196,7 +196,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): last_fn = get_latest_backtest_filename(filename_last.parent) assert re.match(r"btresult-.*\.json", last_fn) - filename1 = Path(tmpdir / last_fn) + filename1 = tmp_path / last_fn assert filename1.is_file() content = filename1.read_text() assert 'max_drawdown_account' in content @@ -254,14 +254,14 @@ def test_store_backtest_candles(testdatadir, mocker): dump_mock.reset_mock() -def test_write_read_backtest_candles(tmpdir): +def test_write_read_backtest_candles(tmp_path): candle_dict = {'DefStrat': {'UNITTEST/BTC': pd.DataFrame()}} # test directory exporting sample_date = '2022_01_01_15_05_13' - store_backtest_analysis_results(Path(tmpdir), candle_dict, {}, sample_date) - stored_file = Path(tmpdir / f'backtest-result-{sample_date}_signals.pkl') + store_backtest_analysis_results(tmp_path, candle_dict, {}, sample_date) + stored_file = tmp_path / f'backtest-result-{sample_date}_signals.pkl' with stored_file.open("rb") as scp: pickled_signal_candles = joblib.load(scp) @@ -273,9 +273,9 @@ def test_write_read_backtest_candles(tmpdir): _clean_test_file(stored_file) # test file exporting - filename = Path(tmpdir / 'testresult') + filename = tmp_path / 'testresult' store_backtest_analysis_results(filename, candle_dict, {}, sample_date) - stored_file = Path(tmpdir / f'testresult-{sample_date}_signals.pkl') + stored_file = tmp_path / f'testresult-{sample_date}_signals.pkl' with stored_file.open("rb") as scp: pickled_signal_candles = joblib.load(scp) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 1e87d3940..f2bb0b2f1 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -29,15 +29,15 @@ def test_init_create_session(default_conf): assert 'scoped_session' in type(Trade.session).__name__ -def test_init_custom_db_url(default_conf, tmpdir): +def test_init_custom_db_url(default_conf, tmp_path): # Update path to a value other than default, but still in-memory - filename = f"{tmpdir}/freqtrade2_test.sqlite" - assert not Path(filename).is_file() + filename = tmp_path / "freqtrade2_test.sqlite" + assert not filename.is_file() default_conf.update({'db_url': f'sqlite:///{filename}'}) init_db(default_conf['db_url']) - assert Path(filename).is_file() + assert filename.is_file() r = Trade.session.execute(text("PRAGMA journal_mode")) assert r.first() == ('wal',) diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 541106ba5..5829f8b71 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -2302,6 +2302,101 @@ def test_recalc_trade_from_orders(fee): assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val +@pytest.mark.usefixtures("init_persistence") +def test_recalc_trade_from_orders_kucoin(): + # Taken from https://github.com/freqtrade/freqtrade/issues/9346 + o1_amount = 11511963.8634448908 + o2_amount = 11750101.7743937783 + o3_amount = 23262065.6378386617 # Exit amount - barely doesn't even out + + res = o1_amount + o2_amount - o3_amount + assert res > 0.0 + assert res < 0.1 + o1_rate = 0.000029901 + o2_rate = 0.000029295 + o3_rate = 0.000029822 + + o1_cost = o1_amount * o1_rate + + trade = Trade( + pair='FLOKI/USDT', + stake_amount=o1_cost, + open_date=dt_now() - timedelta(hours=2), + amount=o1_amount, + fee_open=0.001, + fee_close=0.001, + exchange='binance', + open_rate=o1_rate, + max_rate=o1_rate, + leverage=1, + ) + # Check with 1 order + order1 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=o1_rate, + average=o1_rate, + filled=o1_amount, + remaining=0, + cost=o1_cost, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + trade.orders.append(order1) + order2 = Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=o2_rate, + average=o2_rate, + filled=o2_amount, + remaining=0, + cost=o2_amount * o2_rate, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + trade.orders.append(order2) + trade.recalc_trade_from_orders() + assert trade.amount == o1_amount + o2_amount + profit = trade.calculate_profit(o3_rate) + assert profit.profit_abs == pytest.approx(3.90069871) + assert profit.profit_ratio == pytest.approx(0.00566035) + + order3 = Order( + ft_order_side='sell', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="sell", + price=o3_rate, + average=o3_rate, + filled=o3_amount, + remaining=0, + cost=o2_amount * o2_rate, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + + trade.orders.append(order3) + trade.update_trade(order3) + assert trade.is_open is False + # Trade closed correctly - but left a minimal amount. + assert trade.amount == 8e-09 + assert pytest.approx(trade.close_profit_abs) == 3.90069871 + assert pytest.approx(trade.close_profit) == 0.00566035 + + @pytest.mark.parametrize('is_short', [True, False]) def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): @@ -2581,9 +2676,9 @@ def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open): 'orders': [ (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), - (('sell', 50, 12), (150.0, 12.5, 1875.0, -25.0, -25.0, -0.04)), - (('sell', 100, 20), (50.0, 12.5, 625.0, 725.0, 750.0, 0.60)), - (('sell', 50, 5), (50.0, 12.5, 625.0, 350.0, -375.0, -0.60)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -25.0, -25.0, -0.01)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 725.0, 750.0, 0.29)), + (('sell', 50, 5), (50.0, 12.5, 625.0, 350.0, -375.0, 0.14)), ], 'end_profit': 350.0, 'end_profit_ratio': 0.14, @@ -2593,9 +2688,9 @@ def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open): 'orders': [ (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), - (('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)), - (('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)), - (('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, -377.1875, -0.60199501)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.011197)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.2848129)), + (('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, -377.1875, 0.1343142)), ], 'end_profit': 336.625, 'end_profit_ratio': 0.1343142, @@ -2605,10 +2700,10 @@ def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open): 'orders': [ (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), - (('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)), - (('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)), - (('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)), - (('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 1787.25, 1.08048062)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 0.5945137)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 0.5945137)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.4261653)), + (('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 1787.25, 0.9747170)), ], 'end_profit': 3175.75, 'end_profit_ratio': 0.9747170, @@ -2619,10 +2714,10 @@ def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open): 'orders': [ (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), - (('sell', 100, 11), (100.0, 5.0, 500.0, 600.0, 600.0, 1.2)), - (('buy', 150, 15), (250.0, 11.0, 2750.0, 600.0, 600.0, 1.2)), - (('sell', 100, 19), (150.0, 11.0, 1650.0, 1400.0, 800.0, 0.72727273)), - (('sell', 150, 23), (150.0, 11.0, 1650.0, 3200.0, 1800.0, 1.09090909)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 600.0, 600.0, 0.6)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 600.0, 600.0, 0.6)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1400.0, 800.0, 0.43076923)), + (('sell', 150, 23), (150.0, 11.0, 1650.0, 3200.0, 1800.0, 0.98461538)), ], 'end_profit': 3200.0, 'end_profit_ratio': 0.98461538, @@ -2632,10 +2727,10 @@ def test_order_to_ccxt(limit_buy_order_open, limit_sell_order_usdt_open): 'orders': [ (('buy', 100, 8), (100.0, 8.0, 800.0, 0.0, None, None)), (('buy', 100, 9), (200.0, 8.5, 1700.0, 0.0, None, None)), - (('sell', 100, 10), (100.0, 8.5, 850.0, 150.0, 150.0, 0.17647059)), - (('buy', 150, 11), (250.0, 10, 2500.0, 150.0, 150.0, 0.17647059)), - (('sell', 100, 12), (150.0, 10.0, 1500.0, 350.0, 200.0, 0.2)), - (('sell', 150, 14), (150.0, 10.0, 1500.0, 950.0, 600.0, 0.40)), + (('sell', 100, 10), (100.0, 8.5, 850.0, 150.0, 150.0, 0.08823529)), + (('buy', 150, 11), (250.0, 10, 2500.0, 150.0, 150.0, 0.08823529)), + (('sell', 100, 12), (150.0, 10.0, 1500.0, 350.0, 200.0, 0.1044776)), + (('sell', 150, 14), (150.0, 10.0, 1500.0, 950.0, 600.0, 0.283582)), ], 'end_profit': 950.0, 'end_profit_ratio': 0.283582, diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 8fe8cec6b..0228910f2 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -242,7 +242,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair # 2nd Trade that counts with correct pair generate_mock_trade( pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=180, min_ago_close=30, profit_rate=0.9, is_short=is_short + min_ago_open=180, min_ago_close=31, profit_rate=0.9, is_short=is_short ) freqtrade.protections.stop_per_pair(pair) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4bbd07f18..e94509b40 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1063,6 +1063,63 @@ def test_api_performance(botclient, fee): 'profit_ratio': -0.05570419, 'profit_abs': -0.1150375}] +def test_api_entries(botclient, fee): + ftbot, client = botclient + patch_get_signal(ftbot) + # Empty + rc = client_get(client, f"{BASE_URI}/entries") + assert_response(rc) + assert len(rc.json()) == 0 + + create_mock_trades(fee) + rc = client_get(client, f"{BASE_URI}/entries") + assert_response(rc) + response = rc.json() + assert len(response) == 2 + resp = response[0] + assert resp['enter_tag'] == 'TEST1' + assert resp['count'] == 1 + assert resp['profit_pct'] == 0.5 + + +def test_api_exits(botclient, fee): + ftbot, client = botclient + patch_get_signal(ftbot) + # Empty + rc = client_get(client, f"{BASE_URI}/exits") + assert_response(rc) + assert len(rc.json()) == 0 + + create_mock_trades(fee) + rc = client_get(client, f"{BASE_URI}/exits") + assert_response(rc) + response = rc.json() + assert len(response) == 2 + resp = response[0] + assert resp['exit_reason'] == 'sell_signal' + assert resp['count'] == 1 + assert resp['profit_pct'] == 0.5 + + +def test_api_mix_tag(botclient, fee): + ftbot, client = botclient + patch_get_signal(ftbot) + # Empty + rc = client_get(client, f"{BASE_URI}/mix_tags") + assert_response(rc) + assert len(rc.json()) == 0 + + create_mock_trades(fee) + rc = client_get(client, f"{BASE_URI}/mix_tags") + assert_response(rc) + response = rc.json() + assert len(response) == 2 + resp = response[0] + assert resp['mix_tag'] == 'TEST1 sell_signal' + assert resp['count'] == 1 + assert resp['profit_pct'] == 0.5 + + @pytest.mark.parametrize( 'is_short,current_rate,open_trade_value', [(True, 1.098e-05, 15.0911775), @@ -1616,9 +1673,9 @@ def test_api_plot_config(botclient, mocker): assert_response(rc) -def test_api_strategies(botclient, tmpdir): +def test_api_strategies(botclient, tmp_path): ftbot, client = botclient - ftbot.config['user_data_dir'] = Path(tmpdir) + ftbot.config['user_data_dir'] = tmp_path rc = client_get(client, f"{BASE_URI}/strategies") @@ -1701,9 +1758,9 @@ def test_api_exchanges(botclient): } -def test_api_freqaimodels(botclient, tmpdir, mocker): +def test_api_freqaimodels(botclient, tmp_path, mocker): ftbot, client = botclient - ftbot.config['user_data_dir'] = Path(tmpdir) + ftbot.config['user_data_dir'] = tmp_path mocker.patch( "freqtrade.resolvers.freqaimodel_resolver.FreqaiModelResolver.search_all_objects", return_value=[ @@ -1739,9 +1796,9 @@ def test_api_freqaimodels(botclient, tmpdir, mocker): ]} -def test_api_pairlists_available(botclient, tmpdir): +def test_api_pairlists_available(botclient, tmp_path): ftbot, client = botclient - ftbot.config['user_data_dir'] = Path(tmpdir) + ftbot.config['user_data_dir'] = tmp_path rc = client_get(client, f"{BASE_URI}/pairlists/available") @@ -1768,9 +1825,9 @@ def test_api_pairlists_available(botclient, tmpdir): assert len(volumepl['params']) > 2 -def test_api_pairlists_evaluate(botclient, tmpdir, mocker): +def test_api_pairlists_evaluate(botclient, tmp_path, mocker): ftbot, client = botclient - ftbot.config['user_data_dir'] = Path(tmpdir) + ftbot.config['user_data_dir'] = tmp_path rc = client_get(client, f"{BASE_URI}/pairlists/evaluate/randomJob") @@ -1905,7 +1962,7 @@ def test_sysinfo(botclient): assert 'ram_pct' in result -def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): +def test_api_backtesting(botclient, mocker, fee, caplog, tmp_path): try: ftbot, client = botclient mocker.patch(f'{EXMS}.get_fee', fee) @@ -1935,8 +1992,8 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): assert result['status_msg'] == 'Backtest reset' ftbot.config['export'] = 'trades' ftbot.config['backtest_cache'] = 'day' - ftbot.config['user_data_dir'] = Path(tmpdir) - ftbot.config['exportfilename'] = Path(tmpdir) / "backtest_results" + ftbot.config['user_data_dir'] = tmp_path + ftbot.config['exportfilename'] = tmp_path / "backtest_results" ftbot.config['exportfilename'].mkdir() # start backtesting @@ -2194,14 +2251,14 @@ def test_api_ws_subscribe(botclient, mocker): with client.websocket_connect(ws_url) as ws: ws.send_json({'type': 'subscribe', 'data': ['whitelist']}) - time.sleep(1) + time.sleep(0.2) # Check call count is now 1 as we sent a valid subscribe request assert sub_mock.call_count == 1 with client.websocket_connect(ws_url) as ws: ws.send_json({'type': 'subscribe', 'data': 'whitelist'}) - time.sleep(1) + time.sleep(0.2) # Call count hasn't changed as the subscribe request was invalid assert sub_mock.call_count == 1 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 41c24cc45..b07951c0e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -150,8 +150,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['reload_conf', 'reload_config'], ['show_conf', 'show_config'], " "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " "['bl_delete', 'blacklist_delete'], " - "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir']" - "]") + "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], " + "['order']]") assert log_has(message_str, caplog) @@ -347,8 +347,6 @@ async def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> msg = msg_mock.call_args_list[3][0][0] assert re.search(r'Number of Entries.*2', msg) assert re.search(r'Number of Exits.*1', msg) - assert re.search(r'from 1st entry rate', msg) - assert re.search(r'Order Filled', msg) assert re.search(r'Close Date:', msg) is None assert re.search(r'Close Profit:', msg) is None @@ -375,6 +373,105 @@ async def test_telegram_status_closed_trade(default_conf, update, mocker, fee) - assert re.search(r'Close Profit:', msg) +async def test_order_handle(default_conf, update, ticker, fee, mocker) -> None: + default_conf['max_open_trades'] = 3 + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker, + get_fee=fee, + _dry_is_price_crossed=MagicMock(return_value=True), + ) + status_table = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _status_table=status_table, + ) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot) + + freqtradebot.state = State.RUNNING + msg_mock.reset_mock() + + # Create some test data + freqtradebot.enter_positions() + + mocker.patch('freqtrade.rpc.telegram.MAX_MESSAGE_LENGTH', 500) + + msg_mock.reset_mock() + context = MagicMock() + context.args = ["2"] + await telegram._order(update=update, context=context) + + assert msg_mock.call_count == 1 + + msg1 = msg_mock.call_args_list[0][0][0] + + assert 'Order List for Trade #*`2`' in msg1 + + msg_mock.reset_mock() + mocker.patch('freqtrade.rpc.telegram.MAX_MESSAGE_LENGTH', 50) + context = MagicMock() + context.args = ["2"] + await telegram._order(update=update, context=context) + + assert msg_mock.call_count == 2 + + msg1 = msg_mock.call_args_list[0][0][0] + msg2 = msg_mock.call_args_list[1][0][0] + + assert 'Order List for Trade #*`2`' in msg1 + assert '*Order List for Trade #*`2` - continued' in msg2 + + +@pytest.mark.usefixtures("init_persistence") +async def test_telegram_order_multi_entry(default_conf, update, mocker, fee) -> None: + default_conf['telegram']['enabled'] = False + default_conf['position_adjustment_enable'] = True + mocker.patch.multiple( + EXMS, + fetch_order=MagicMock(return_value=None), + get_rate=MagicMock(return_value=0.22), + ) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + create_mock_trades(fee) + trades = Trade.get_open_trades() + trade = trades[3] + # Average may be empty on some exchanges + trade.orders[0].average = 0 + trade.orders.append(Order( + order_id='5412vbb', + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + ft_amount=trade.amount, + ft_price=trade.open_rate, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=trade.open_rate * 0.95, + average=0, + filled=trade.amount, + remaining=0, + cost=trade.amount, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + ) + trade.recalc_trade_from_orders() + Trade.commit() + + await telegram._order(update=update, context=MagicMock()) + assert msg_mock.call_count == 4 + msg = msg_mock.call_args_list[3][0][0] + assert re.search(r'from 1st entry rate', msg) + assert re.search(r'Order Filled', msg) + + async def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 3 mocker.patch.multiple( @@ -443,14 +540,12 @@ async def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: context.args = ["2"] await telegram._status(update=update, context=context) - assert msg_mock.call_count == 2 + assert msg_mock.call_count == 1 msg1 = msg_mock.call_args_list[0][0][0] - msg2 = msg_mock.call_args_list[1][0][0] assert 'Close Rate' not in msg1 assert 'Trade ID:* `2`' in msg1 - assert 'Trade ID:* `2` - continued' in msg2 async def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: @@ -1359,10 +1454,19 @@ async def test_force_enter_no_pair(default_conf, update, mocker) -> None: assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 update = MagicMock() update.callback_query = AsyncMock() - update.callback_query.data = 'XRP/USDT_||_long' + update.callback_query.data = 'force_enter__XRP/USDT_||_long' await telegram._force_enter_inline(update, None) assert fbuy_mock.call_count == 1 + fbuy_mock.reset_mock() + update.callback_query = AsyncMock() + update.callback_query.data = 'force_enter__cancel' + await telegram._force_enter_inline(update, None) + assert fbuy_mock.call_count == 0 + query = update.callback_query + assert query.edit_message_text.call_count == 1 + assert query.edit_message_text.call_args_list[-1][1]['text'] == "Force enter canceled." + async def test_telegram_performance_handle(default_conf_usdt, update, ticker, fee, mocker) -> None: diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py index 8c1466de9..f34eddc69 100644 --- a/tests/strategy/strats/informative_decorator_strategy.py +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -47,6 +47,11 @@ class InformativeDecoratorTest(IStrategy): dataframe['rsi'] = 14 return dataframe + @informative('1h', '{base}/BTC') + def populate_indicators_base_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + # Quote currency different from stake currency test. @informative('1h', 'ETH/BTC', candle_type='spot') def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 535b3dbd6..2f611a6c6 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -277,9 +277,11 @@ def test_informative_decorator(mocker, default_conf_usdt, trading_mode): ('XRP/USDT', '5m', candle_def): test_data_5m, ('XRP/USDT', '30m', candle_def): test_data_30m, ('XRP/USDT', '1h', candle_def): test_data_1h, + ('XRP/BTC', '1h', candle_def): test_data_1h, # from {base}/BTC ('LTC/USDT', '5m', candle_def): test_data_5m, ('LTC/USDT', '30m', candle_def): test_data_30m, ('LTC/USDT', '1h', candle_def): test_data_1h, + ('LTC/BTC', '1h', candle_def): test_data_1h, # from {base}/BTC ('NEO/USDT', '30m', candle_def): test_data_30m, ('NEO/USDT', '5m', CandleType.SPOT): test_data_5m, # Explicit request with '' as candletype ('NEO/USDT', '15m', candle_def): test_data_5m, # Explicit request with '' as candletype @@ -296,10 +298,12 @@ def test_informative_decorator(mocker, default_conf_usdt, trading_mode): 'XRP/USDT', 'LTC/USDT', 'NEO/USDT' ]) - assert len(strategy._ft_informative) == 6 # Equal to number of decorators used + assert len(strategy._ft_informative) == 7 # Equal to number of decorators used informative_pairs = [ ('XRP/USDT', '1h', candle_def), + ('XRP/BTC', '1h', candle_def), ('LTC/USDT', '1h', candle_def), + ('LTC/BTC', '1h', candle_def), ('XRP/USDT', '30m', candle_def), ('LTC/USDT', '30m', candle_def), ('NEO/USDT', '1h', candle_def), diff --git a/tests/test_binance_mig.py b/tests/test_binance_mig.py index dc89a1ae7..b7c821a5a 100644 --- a/tests/test_binance_mig.py +++ b/tests/test_binance_mig.py @@ -1,7 +1,6 @@ import shutil -from pathlib import Path import pytest @@ -10,7 +9,7 @@ from freqtrade.util.binance_mig import migrate_binance_futures_data, migrate_bin from tests.conftest import create_mock_trades_usdt, log_has -def test_binance_mig_data_conversion(default_conf_usdt, tmpdir, testdatadir): +def test_binance_mig_data_conversion(default_conf_usdt, tmp_path, testdatadir): # call doing nothing (spot mode) migrate_binance_futures_data(default_conf_usdt) @@ -18,7 +17,7 @@ def test_binance_mig_data_conversion(default_conf_usdt, tmpdir, testdatadir): pair_old = 'XRP_USDT' pair_unified = 'XRP_USDT_USDT' futures_src = testdatadir / 'futures' - futures_dst = tmpdir / 'futures' + futures_dst = tmp_path / 'futures' futures_dst.mkdir() files = [ '-1h-mark.feather', @@ -32,7 +31,7 @@ def test_binance_mig_data_conversion(default_conf_usdt, tmpdir, testdatadir): fn_after = futures_dst / f'{pair_old}{file}' shutil.copy(futures_src / f'{pair_unified}{file}', fn_after) - default_conf_usdt['datadir'] = Path(tmpdir) + default_conf_usdt['datadir'] = tmp_path # Migrate files to unified namings migrate_binance_futures_data(default_conf_usdt) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 3f2fb0669..6472faf37 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -104,8 +104,8 @@ def test_load_config_file_error_range(default_conf, mocker, caplog) -> None: assert x == '' -def test_load_file_error(tmpdir): - testpath = Path(tmpdir) / 'config.json' +def test_load_file_error(tmp_path): + testpath = tmp_path / 'config.json' with pytest.raises(OperationalException, match=r"File .* not found!"): load_file(testpath) @@ -601,9 +601,9 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: assert log_has('Verbosity set to 3', caplog) -def test_set_logfile(default_conf, mocker, tmpdir): +def test_set_logfile(default_conf, mocker, tmp_path): patched_configuration_load_config_file(mocker, default_conf) - f = Path(tmpdir / "test_file.log") + f = tmp_path / "test_file.log" assert not f.is_file() arglist = [ 'trade', '--logfile', str(f), @@ -1145,7 +1145,7 @@ def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): configuration.get_config() -def test_pairlist_resolving_fallback(mocker, tmpdir): +def test_pairlist_resolving_fallback(mocker, tmp_path): mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) mocker.patch("freqtrade.configuration.configuration.load_file", @@ -1164,7 +1164,7 @@ def test_pairlist_resolving_fallback(mocker, tmpdir): assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] assert config['exchange']['name'] == 'binance' - assert config['datadir'] == Path(tmpdir) / "user_data/data/binance" + assert config['datadir'] == tmp_path / "user_data/data/binance" @pytest.mark.parametrize("setting", [ diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index 8e49aab10..8bd07f18a 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -32,9 +32,9 @@ def test_create_userdata_dir(mocker, default_conf, caplog) -> None: assert str(x) == str(Path("/tmp/bar")) -def test_create_userdata_dir_and_chown(mocker, tmpdir, caplog) -> None: +def test_create_userdata_dir_and_chown(mocker, tmp_path, caplog) -> None: sp_mock = mocker.patch('subprocess.check_output') - path = Path(tmpdir / 'bar') + path = tmp_path / 'bar' assert not path.is_dir() x = create_userdata_dir(str(path), create_dir=True) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 40d77ce6c..c9cb86cc0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -6569,16 +6569,16 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: # tuple 2 - amount, open_rate, stake_amount, cumulative_profit, realized_profit, rel_profit (('buy', 100, 10), (100.0, 10.0, 1000.0, 0.0, None, None)), (('buy', 100, 15), (200.0, 12.5, 2500.0, 0.0, None, None)), - (('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.044788)), - (('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.59201995)), + (('sell', 50, 12), (150.0, 12.5, 1875.0, -28.0625, -28.0625, -0.011197)), + (('sell', 100, 20), (50.0, 12.5, 625.0, 713.8125, 741.875, 0.2848129)), (('sell', 50, 5), (50.0, 12.5, 625.0, 336.625, 336.625, 0.1343142)), # final profit (sum) ), ( (('buy', 100, 3), (100.0, 3.0, 300.0, 0.0, None, None)), (('buy', 100, 7), (200.0, 5.0, 1000.0, 0.0, None, None)), - (('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 1.189027)), - (('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 1.189027)), - (('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.7186579)), + (('sell', 100, 11), (100.0, 5.0, 500.0, 596.0, 596.0, 0.5945137)), + (('buy', 150, 15), (250.0, 11.0, 2750.0, 596.0, 596.0, 0.5945137)), + (('sell', 100, 19), (150.0, 11.0, 1650.0, 1388.5, 792.5, 0.4261653)), (('sell', 150, 23), (150.0, 11.0, 1650.0, 3175.75, 3175.75, 0.9747170)), # final profit ) ]) diff --git a/tests/test_integration.py b/tests/test_integration.py index ee1d4bbb3..12647f6e2 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,3 +1,4 @@ +import time from unittest.mock import MagicMock import pytest @@ -440,6 +441,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert trade.open_rate == 1.99 assert trade.orders[-1].price == 1.96 assert trade.orders[-1].cost == 120 * leverage + time.sleep(0.1) # Replace new order with diff. order at a lower price freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1.95) diff --git a/tests/test_log_setup.py b/tests/test_log_setup.py index bd3399615..182e78730 100644 --- a/tests/test_log_setup.py +++ b/tests/test_log_setup.py @@ -1,6 +1,5 @@ import logging import sys -from pathlib import Path import pytest @@ -75,11 +74,11 @@ def test_set_loggers_syslog(): @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") -def test_set_loggers_Filehandler(tmpdir): +def test_set_loggers_Filehandler(tmp_path): logger = logging.getLogger() orig_handlers = logger.handlers logger.handlers = [] - logfile = Path(tmpdir) / 'ft_logfile.log' + logfile = tmp_path / 'ft_logfile.log' config = {'verbosity': 2, 'logfile': str(logfile), } diff --git a/tests/test_strategy_updater.py b/tests/test_strategy_updater.py index 3b48c952c..7f4ae4349 100644 --- a/tests/test_strategy_updater.py +++ b/tests/test_strategy_updater.py @@ -40,7 +40,7 @@ def test_strategy_updater_start(user_dir, capsys) -> None: # Backup file exists assert Path(user_dir / "strategies_orig_updater" / 'strategy_test_v2.py').exists() # updated file exists - new_file = Path(tmpdirp / 'strategy_test_v2.py') + new_file = tmpdirp / 'strategy_test_v2.py' assert new_file.exists() new_code = new_file.read_text() assert 'INTERFACE_VERSION = 3' in new_code diff --git a/tests/testdata/config.tests.json b/tests/testdata/config.tests.json new file mode 100644 index 000000000..fdfa97350 --- /dev/null +++ b/tests/testdata/config.tests.json @@ -0,0 +1,74 @@ +{ + "max_open_trades": 3, + "stake_currency": "BTC", + "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "timeframe": "5m", + "dry_run": true, + "cancel_open_orders_on_exit": false, + "unfilledtimeout": { + "entry": 5, + "exit": 5, + "exit_timeout_count": 0, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing":{ + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "gate", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "ETH/BTC", + "LTC/BTC", + "ETC/BTC", + "XLM/BTC", + "XRP/BTC", + "ADA/BTC", + "DOT/BTC" + ], + "pair_blacklist": [ + "DOGE/BTC" + ] + }, + "pairlists": [ + {"method": "StaticPairList"} + ], + "telegram": { + "enabled": false, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "jwt_secret_key": "somethingrandom", + "CORS_origins": [], + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "bot_name": "freqtrade", + "initial_state": "running", + "force_entry_enable": false, + "internals": { + "process_throttle_secs": 5 + } +}