diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c80bc141..7e4487ac8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: - name: Installation - *nix if: runner.os == 'Linux' run: | - python -m pip install --upgrade pip wheel + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include @@ -163,7 +163,7 @@ jobs: rm /usr/local/bin/python3.11-config || true brew install hdf5 c-blosc - python -m pip install --upgrade pip wheel + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include @@ -352,7 +352,7 @@ jobs: - name: Installation - *nix if: runner.os == 'Linux' run: | - python -m pip install --upgrade pip wheel + python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_INCLUDE_PATH=${HOME}/dependencies/include @@ -425,7 +425,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.3 + uses: pypa/gh-action-pypi-publish@v1.8.5 if: (github.event_name == 'release') with: user: __token__ @@ -433,7 +433,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.3 + uses: pypa/gh-action-pypi-publish@v1.8.5 if: (github.event_name == 'release') with: user: __token__ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4784055a9..0031300cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,12 +13,12 @@ repos: - id: mypy exclude: build_helpers additional_dependencies: - - types-cachetools==5.3.0.4 + - types-cachetools==5.3.0.5 - types-filelock==3.2.7 - - types-requests==2.28.11.16 - - types-tabulate==0.9.0.1 - - types-python-dateutil==2.8.19.10 - - SQLAlchemy==2.0.7 + - types-requests==2.28.11.17 + - types-tabulate==0.9.0.2 + - types-python-dateutil==2.8.19.12 + - SQLAlchemy==2.0.10 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/Dockerfile b/Dockerfile index 6a4a168c1..ee8b3f0a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.10.10-slim-bullseye as base +FROM python:3.10.11-slim-bullseye as base # Setup env ENV LANG C.UTF-8 @@ -25,7 +25,7 @@ FROM base as python-deps RUN apt-get update \ && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get clean \ - && pip install --upgrade pip + && pip install --upgrade pip==23.0.1 wheel==0.38.4 # Install TA-lib COPY build_helpers/* /tmp/ diff --git a/README.md b/README.md index c8bc50dac..57c4e3a52 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,6 @@ To run this bot we recommend you a cloud instance with a minimum of: - [Python >= 3.8](http://docs.python-guide.org/en/latest/starting/installation/) - [pip](https://pip.pypa.io/en/stable/installing/) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) +- [TA-Lib](https://ta-lib.github.io/ta-lib-python/) - [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) - [Docker](https://www.docker.com/products/docker) (Recommended) diff --git a/build_helpers/TA_Lib-0.4.25-cp310-cp310-win_amd64.whl b/build_helpers/TA_Lib-0.4.25-cp310-cp310-win_amd64.whl deleted file mode 100644 index c6435da0d..000000000 Binary files a/build_helpers/TA_Lib-0.4.25-cp310-cp310-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.25-cp311-cp311-win_amd64.whl b/build_helpers/TA_Lib-0.4.25-cp311-cp311-win_amd64.whl deleted file mode 100644 index 4d55d18c8..000000000 Binary files a/build_helpers/TA_Lib-0.4.25-cp311-cp311-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.25-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.25-cp38-cp38-win_amd64.whl deleted file mode 100644 index f2806db80..000000000 Binary files a/build_helpers/TA_Lib-0.4.25-cp38-cp38-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.25-cp39-cp39-win_amd64.whl b/build_helpers/TA_Lib-0.4.25-cp39-cp39-win_amd64.whl deleted file mode 100644 index 0d4ceb3b4..000000000 Binary files a/build_helpers/TA_Lib-0.4.25-cp39-cp39-win_amd64.whl and /dev/null differ diff --git a/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl new file mode 100644 index 000000000..466455a8c Binary files /dev/null and b/build_helpers/TA_Lib-0.4.26-cp310-cp310-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl new file mode 100644 index 000000000..16f4f411a Binary files /dev/null and b/build_helpers/TA_Lib-0.4.26-cp311-cp311-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl new file mode 100644 index 000000000..324502fb8 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.26-cp38-cp38-win_amd64.whl differ diff --git a/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl b/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl new file mode 100644 index 000000000..389403041 Binary files /dev/null and b/build_helpers/TA_Lib-0.4.26-cp39-cp39-win_amd64.whl differ diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 36606cc3a..3e7df5dfc 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -1,21 +1,21 @@ # Downloads don't work automatically, since the URL is regenerated via javascript. # Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib -python -m pip install --upgrade pip wheel +python -m pip install --upgrade pip==23.0.1 wheel==0.38.4 $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" if ($pyv -eq '3.8') { - pip install build_helpers\TA_Lib-0.4.25-cp38-cp38-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.26-cp38-cp38-win_amd64.whl } if ($pyv -eq '3.9') { - pip install build_helpers\TA_Lib-0.4.25-cp39-cp39-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.26-cp39-cp39-win_amd64.whl } if ($pyv -eq '3.10') { - pip install build_helpers\TA_Lib-0.4.25-cp310-cp310-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.26-cp310-cp310-win_amd64.whl } if ($pyv -eq '3.11') { - pip install build_helpers\TA_Lib-0.4.25-cp311-cp311-win_amd64.whl + pip install build_helpers\TA_Lib-0.4.26-cp311-cp311-win_amd64.whl } pip install -r requirements-dev.txt pip install -e . diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index a6ecdbee6..8f0de2cc9 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -12,6 +12,7 @@ TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG_PLOT=${TAG}_plot TAG_FREQAI=${TAG}_freqai TAG_FREQAI_RL=${TAG_FREQAI}rl +TAG_FREQAI_TORCH=${TAG_FREQAI}torch TAG_PI="${TAG}_pi" TAG_ARM=${TAG}_arm @@ -42,9 +43,9 @@ if [ $? -ne 0 ]; then return 1 fi -docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . -docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai . -docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl . # Tag image for upload and next build step docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM @@ -84,6 +85,10 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} +# Create special Torch tag - which is identical to the RL tag. +docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_TORCH} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} +docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_TORCH} + # copy images to ghcr.io alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane" @@ -93,6 +98,7 @@ chmod a+rwx .crane echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL} +crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_TORCH} crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI} crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT} crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG} diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index 27fa06b95..72b20ac5d 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -58,9 +58,9 @@ fi # Tag image for upload and next build step docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG -docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . -docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai . -docker build --cache-from freqtrade:${TAG_FREQAI} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG} -t freqtrade:${TAG_FREQAI} -f docker/Dockerfile.freqai . +docker build --build-arg sourceimage=freqtrade --build-arg sourcetag=${TAG_FREQAI} -t freqtrade:${TAG_FREQAI_RL} -f docker/Dockerfile.freqai_rl . docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT docker tag freqtrade:$TAG_FREQAI ${CACHE_IMAGE}:$TAG_FREQAI diff --git a/docs/assets/freqai_pytorch-diagram.png b/docs/assets/freqai_pytorch-diagram.png new file mode 100644 index 000000000..f48ebae25 Binary files /dev/null and b/docs/assets/freqai_pytorch-diagram.png differ diff --git a/docs/backtesting.md b/docs/backtesting.md index 0227df3f6..166c2b28b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -274,19 +274,20 @@ A backtesting result will look like that: | XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | | ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | -========================================================= EXIT REASON STATS ========================================================== -| Exit Reason | Exits | Wins | Draws | Losses | -|:-------------------|--------:|------:|-------:|--------:| -| trailing_stop_loss | 205 | 150 | 0 | 55 | -| stop_loss | 166 | 0 | 0 | 166 | -| exit_signal | 56 | 36 | 0 | 20 | -| force_exit | 2 | 0 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== | Pair | Entries | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | |:---------|---------:|---------------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| | ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | | LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | | TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | +==================== EXIT REASON STATS ==================== +| Exit Reason | Exits | Wins | Draws | Losses | +|:-------------------|--------:|------:|-------:|--------:| +| trailing_stop_loss | 205 | 150 | 0 | 55 | +| stop_loss | 166 | 0 | 0 | 166 | +| exit_signal | 56 | 36 | 0 | 20 | +| force_exit | 2 | 0 | 0 | 2 | + ================== SUMMARY METRICS ================== | Metric | Value | |-----------------------------+---------------------| diff --git a/docs/configuration.md b/docs/configuration.md index 8a1aeb40e..cf3872f1c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -138,7 +138,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_currency` | **Required.** Crypto-currency used for trading.
**Datatype:** String | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float or `"unlimited"`. | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
**Datatype:** Positive float between `0.1` and `1.0`. -| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account.[More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. +| `available_capital` | Available starting capital for the bot. Useful when running multiple bots on the same exchange account. [More information below](#configuring-amount-per-trade).
**Datatype:** Positive float. | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
**Datatype:** Positive Float as ratio. @@ -155,25 +155,25 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
**Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling.
**Datatype:** Float (as ratio) -| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates)
*Defaults to None.*
**Datatype:** Float +| `futures_funding_rate` | User-specified funding rate to be used when historical funding rates are not available from the exchange. This does not overwrite real historical rates. It is recommended that this be set to 0 unless you are testing a specific coin and you understand how the funding rate will affect freqtrade's profit calculations. [More information here](leverage.md#unavailable-funding-rates)
*Defaults to `None`.*
**Datatype:** Float | `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md).
*Defaults to `"spot"`.*
**Datatype:** String | `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md).
**Datatype:** String | `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md).
*Defaults to `0.05`.*
**Datatype:** Float | | **Unfilled timeout** | `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer | `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Integer -| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `minutes`.*
**Datatype:** String +| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy).
*Defaults to `"minutes"`.*
**Datatype:** String | `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency exit is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0`.*
**Datatype:** Integer | | **Pricing** -| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#entry-price).
*Defaults to `"same"`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled). -| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean +| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `true`.*
**Datatype:** Boolean | `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled).
*Defaults to `1`.*
**Datatype:** Positive Integer | `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
**Datatype:** Boolean | `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
**Datatype:** Float (as ratio) -| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).
*Defaults to `same`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). +| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).
*Defaults to `"same"`.*
**Datatype:** String (either `ask`, `bid`, `same` or `other`). | `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled). -| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled).
*Defaults to `True`.*
**Datatype:** Boolean +| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled).
*Defaults to `true`.*
**Datatype:** Boolean | `exit_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to exit. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Exit](#exit-price-with-orderbook-enabled)
*Defaults to `1`.*
**Datatype:** Positive Integer | `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price.
*Defaults to `0.02` 2%).*
**Datatype:** Positive float | | **TODO** @@ -199,10 +199,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer -| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean -| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`*
**Datatype:** Boolean +| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`*
**Datatype:** Boolean | `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".
*Defaults to `None`
**Datatype:** float -| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`*
**Datatype:** Boolean | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | | **Plugins** | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation of all possible configuration options. @@ -213,7 +213,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `telegram.balance_dust_level` | Dust-level (in stake currency) - currencies with a balance below this will not be shown by `/balance`.
**Datatype:** float -| `telegram.reload` | Allow "reload" buttons on telegram messages.
*Defaults to `True`.
**Datatype:** boolean +| `telegram.reload` | Allow "reload" buttons on telegram messages.
*Defaults to `true`.
**Datatype:** boolean | `telegram.notification_settings.*` | Detailed notification settings. Refer to the [telegram documentation](telegram-usage.md) for details.
**Datatype:** dictionary | `telegram.allow_custom_messages` | Enable the sending of Telegram messages from strategies via the dataprovider.send_msg() function.
**Datatype:** Boolean | | **Webhook** diff --git a/docs/faq.md b/docs/faq.md index b52a77c6b..7b8cc2580 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -142,6 +142,13 @@ To fix this, redefine order types in the strategy to use "limit" instead of "mar The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy. +### I'm trying to start the bot live, but get an API permission error + +Errors like `Invalid API-key, IP, or permissions for action` mean exactly what they actually say. +Your API key is either invalid (copy/paste error? check for leading/trailing spaces in the config), expired, or the IP you're running the bot from is not enabled in the Exchange's API console. +Usually, the permission "Spot Trading" (or the equivalent in the exchange you use) will be necessary. +Futures will usually have to be enabled specifically. + ### How do I search the bot logs for something? By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. diff --git a/docs/freqai-configuration.md b/docs/freqai-configuration.md index 886dc2338..e7aca20be 100644 --- a/docs/freqai-configuration.md +++ b/docs/freqai-configuration.md @@ -52,7 +52,7 @@ The FreqAI strategy requires including the following lines of code in the standa return dataframe - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -77,7 +77,7 @@ The FreqAI strategy requires including the following lines of code in the standa return dataframe - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -101,7 +101,7 @@ The FreqAI strategy requires including the following lines of code in the standa dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -122,7 +122,7 @@ The FreqAI strategy requires including the following lines of code in the standa dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 return dataframe - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -139,6 +139,7 @@ The FreqAI strategy requires including the following lines of code in the standa / dataframe["close"] - 1 ) + return dataframe ``` Notice how the `feature_engineering_*()` is where [features](freqai-feature-engineering.md#feature-engineering) are added. Meanwhile `set_freqai_targets()` adds the labels/targets. A full example strategy is available in `templates/FreqaiExampleStrategy.py`. @@ -236,3 +237,161 @@ If you want to predict multiple targets you must specify all labels in the same df['&s-up_or_down'] = np.where( df["close"].shift(-100) > df["close"], 'up', 'down') df['&s-up_or_down'] = np.where( df["close"].shift(-100) == df["close"], 'same', df['&s-up_or_down']) ``` + +## PyTorch Module + +### Quick start + +The easiest way to quickly run a pytorch model is with the following command (for regression task): + +```bash +freqtrade trade --config config_examples/config_freqai.example.json --strategy FreqaiExampleStrategy --freqaimodel PyTorchMLPRegressor --strategy-path freqtrade/templates +``` + +!!! note "Installation/docker" + The PyTorch module requires large packages such as `torch`, which should be explicitly requested during `./setup.sh -i` by answering "y" to the question "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]?". + Users who prefer docker should ensure they use the docker image appended with `_freqaitorch`. + +### Structure + +#### Model + +You can construct your own Neural Network architecture in PyTorch by simply defining your `nn.Module` class inside your custom [`IFreqaiModel` file](#using-different-prediction-models) and then using that class in your `def train()` function. Here is an example of logistic regression model implementation using PyTorch (should be used with nn.BCELoss criterion) for classification tasks. + +```python + +class LogisticRegression(nn.Module): + def __init__(self, input_size: int): + super().__init__() + # Define your layers + self.linear = nn.Linear(input_size, 1) + self.activation = nn.Sigmoid() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + # Define the forward pass + out = self.linear(x) + out = self.activation(out) + return out + +class MyCoolPyTorchClassifier(BasePyTorchClassifier): + """ + This is a custom IFreqaiModel showing how a user might setup their own + custom Neural Network architecture for their training. + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = LogisticRegression( + input_dim=n_features + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + data_convertor=self.data_convertor, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer + +``` + +#### Trainer + +The `PyTorchModelTrainer` performs the idiomatic PyTorch train loop: +Define our model, loss function, and optimizer, and then move them to the appropriate device (GPU or CPU). Inside the loop, we iterate through the batches in the dataloader, move the data to the device, compute the prediction and loss, backpropagate, and update the model parameters using the optimizer. + +In addition, the trainer is responsible for the following: + - saving and loading the model + - converting the data from `pandas.DataFrame` to `torch.Tensor`. + +#### Integration with Freqai module + +Like all freqai models, PyTorch models inherit `IFreqaiModel`. `IFreqaiModel` declares three abstract methods: `train`, `fit`, and `predict`. we implement these methods in three levels of hierarchy. +From top to bottom: + +1. `BasePyTorchModel` - Implements the `train` method. all `BasePyTorch*` inherit it. responsible for general data preparation (e.g., data normalization) and calling the `fit` method. Sets `device` attribute used by children classes. Sets `model_type` attribute used by the parent class. +2. `BasePyTorch*` - Implements the `predict` method. Here, the `*` represents a group of algorithms, such as classifiers or regressors. responsible for data preprocessing, predicting, and postprocessing if needed. +3. `PyTorch*Classifier` / `PyTorch*Regressor` - implements the `fit` method. responsible for the main train flaw, where we initialize the trainer and model objects. + +![image](assets/freqai_pytorch-diagram.png) + +#### Full example + +Building a PyTorch regressor using MLP (multilayer perceptron) model, MSELoss criterion, and AdamW optimizer. + +```python +class PyTorchMLPRegressor(BasePyTorchRegressor): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + init_model=init_model, + target_tensor_type=torch.float, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary) + return trainer +``` + +Here we create a `PyTorchMLPRegressor` class that implements the `fit` method. The `fit` method specifies the training building blocks: model, optimizer, criterion, and trainer. We inherit both `BasePyTorchRegressor` and `BasePyTorchModel`, where the former implements the `predict` method that is suitable for our regression task, and the latter implements the train method. + +??? Note "Setting Class Names for Classifiers" + When using classifiers, the user must declare the class names (or targets) by overriding the `IFreqaiModel.class_names` attribute. This is achieved by setting `self.freqai.class_names` in the FreqAI strategy inside the `set_freqai_targets` method. + + For example, if you are using a binary classifier to predict price movements as up or down, you can set the class names as follows: + ```python + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: + self.freqai.class_names = ["down", "up"] + dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > + dataframe["close"], 'up', 'down') + + return dataframe + ``` + To see a full example, you can refer to the [classifier test strategy class](https://github.com/freqtrade/freqtrade/blob/develop/tests/strategy/strats/freqai_test_classifier.py). diff --git a/docs/freqai-feature-engineering.md b/docs/freqai-feature-engineering.md index 6389bd9e5..82b7569a5 100644 --- a/docs/freqai-feature-engineering.md +++ b/docs/freqai-feature-engineering.md @@ -6,8 +6,8 @@ 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_all()` | This optional function will automatically expand the defined features on the config defined `indicator_periods_candles`, `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. +| `feature_engineering_expand_basic()` | This optional function will automatically expand the defined features on the config defined `include_timeframes`, `include_shifted_candles`, and `include_corr_pairs`. Note: this function does *not* expand across `include_periods_candles`. | `feature_engineering_standard()` | This optional function will be called once with the dataframe of the base timeframe. This is the final function to be called, which means that the dataframe entering this function will contain all the features and columns from the base asset created by the other `feature_engineering_expand` functions. This function is a good place to do custom exotic feature extractions (e.g. tsfresh). This function is also a good place for any feature that should not be auto-expanded upon (e.g., day of the week). | `set_freqai_targets()` | Required function to set the targets for the model. All targets must be prepended with `&` to be recognized by the FreqAI internals. @@ -16,7 +16,7 @@ Meanwhile, high level feature engineering is handled within `"feature_parameters It is advisable to start from the template `feature_engineering_*` functions in the source provided example strategy (found in `templates/FreqaiExampleStrategy.py`) to ensure that the feature definitions are following the correct conventions. Here is an example of how to set the indicators and labels in the strategy: ```python - def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs): + def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -67,7 +67,7 @@ It is advisable to start from the template `feature_engineering_*` functions in return dataframe - def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -96,7 +96,7 @@ It is advisable to start from the template `feature_engineering_*` functions in dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe, metadata, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -122,7 +122,7 @@ It is advisable to start from the template `feature_engineering_*` functions in dataframe["%-hour_of_day"] = (dataframe["date"].dt.hour + 1) / 25 return dataframe - def set_freqai_targets(self, dataframe, metadata, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -181,15 +181,14 @@ You can ask for each of the defined features to be included also for informative In total, the number of features the user of the presented example strat has created is: length of `include_timeframes` * no. features in `feature_engineering_expand_*()` * length of `include_corr_pairlist` * no. `include_shifted_candles` * length of `indicator_periods_candles` $= 3 * 3 * 3 * 2 * 2 = 108$. +### Gain finer control over `feature_engineering_*` functions with `metadata` - ### Gain finer control over `feature_engineering_*` functions with `metadata` +All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc. - All `feature_engineering_*` and `set_freqai_targets()` functions are passed a `metadata` dictionary which contains information about the `pair`, `tf` (timeframe), and `period` that FreqAI is automating for feature building. As such, a user can use `metadata` inside `feature_engineering_*` functions as criteria for blocking/reserving features for certain timeframes, periods, pairs etc. - - ```py -def feature_engineering_expand_all(self, dataframe, period, metadata, **kwargs): - if metadata["tf"] == "1h": - dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) +```python +def feature_engineering_expand_all(self, dataframe: DataFrame, period, metadata, **kwargs) -> DataFrame: + if metadata["tf"] == "1h": + dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) ``` This will block `ta.ROC()` from being added to any timeframes other than `"1h"`. diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 9822a895a..1487b92c2 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -85,6 +85,28 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `net_arch` | Network architecture which is well described in [`stable_baselines3` doc](https://stable-baselines3.readthedocs.io/en/master/guide/custom_policy.html#examples). In summary: `[, dict(vf=[], pi=[])]`. By default this is set to `[128, 128]`, which defines 2 shared hidden layers with 128 units each. | `randomize_starting_position` | Randomize the starting point of each episode to avoid overfitting.
**Datatype:** bool.
Default: `False`. | `drop_ohlc_from_features` | Do not include the normalized ohlc data in the feature set passed to the agent during training (ohlc will still be used for driving the environment in all cases)
**Datatype:** Boolean.
**Default:** `False` +| `progress_bar` | Display a progress bar with the current progress, elapsed time and estimated remaining time.
**Datatype:** Boolean.
Default: `False`. + +### PyTorch parameters + +#### general + +| Parameter | Description | +|------------|-------------| +| | **Model training parameters within the `freqai.model_training_parameters` sub dictionary** +| `learning_rate` | Learning rate to be passed to the optimizer.
**Datatype:** float.
Default: `3e-4`. +| `model_kwargs` | Parameters to be passed to the model class.
**Datatype:** dict.
Default: `{}`. +| `trainer_kwargs` | Parameters to be passed to the trainer class.
**Datatype:** dict.
Default: `{}`. + +#### trainer_kwargs + +| Parameter | Description | +|------------|-------------| +| | **Model training parameters within the `freqai.model_training_parameters.model_kwargs` sub dictionary** +| `max_iters` | The number of training iterations to run. iteration here refers to the number of times we call self.optimizer.step(). used to calculate n_epochs.
**Datatype:** int.
Default: `100`. +| `batch_size` | The size of the batches to use during training..
**Datatype:** int.
Default: `64`. +| `max_n_eval_batches` | The maximum number batches to use for evaluation..
**Datatype:** int, optional.
Default: `None`. + ### Additional parameters diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index f5679a4ba..962827348 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -37,7 +37,7 @@ freqtrade trade --freqaimodel ReinforcementLearner --strategy MyRLStrategy --con where `ReinforcementLearner` will use the templated `ReinforcementLearner` from `freqai/prediction_models/ReinforcementLearner` (or a custom user defined one located in `user_data/freqaimodels`). The strategy, on the other hand, follows the same base [feature engineering](freqai-feature-engineering.md) with `feature_engineering_*` as a typical Regressor. The difference lies in the creation of the targets, Reinforcement Learning doesn't require them. However, FreqAI requires a default (neutral) value to be set in the action column: ```python - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -53,17 +53,19 @@ where `ReinforcementLearner` will use the templated `ReinforcementLearner` from # For RL, there are no direct targets to set. This is filler (neutral) # until the agent sends an action. dataframe["&-action"] = 0 + return dataframe ``` Most of the function remains the same as for typical Regressors, however, the function below shows how the strategy must pass the raw price data to the agent so that it has access to raw OHLCV in the training environment: ```python - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: # The following features are necessary for RL models dataframe[f"%-raw_close"] = dataframe["close"] dataframe[f"%-raw_open"] = dataframe["open"] dataframe[f"%-raw_high"] = dataframe["high"] dataframe[f"%-raw_low"] = dataframe["low"] + return dataframe ``` Finally, there is no explicit "label" to make - instead it is necessary to assign the `&-action` column which will contain the agent's actions when accessed in `populate_entry/exit_trends()`. In the present example, the neutral action to 0. This value should align with the environment used. FreqAI provides two environments, both use 0 as the neutral action. @@ -180,7 +182,7 @@ As you begin to modify the strategy and the prediction model, you will quickly r # you can use feature values from dataframe # Assumes the shifted RSI indicator has been generated in the strategy. - rsi_now = self.raw_features[f"%-rsi-period-10_shift-1_{pair}_" + rsi_now = self.raw_features[f"%-rsi-period_10_shift-1_{pair}_" f"{self.config['timeframe']}"].iloc[self._current_tick] # reward agent for entering trades diff --git a/docs/installation.md b/docs/installation.md index 6e8488b9f..11de20e83 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -52,7 +52,7 @@ These requirements apply to both [Script Installation](#script-installation) and * [pip](https://pip.pypa.io/en/stable/installing/) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) -* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions [below](#install-ta-lib)) +* [TA-Lib](https://ta-lib.github.io/ta-lib-python/) (install instructions [below](#install-ta-lib)) ### Install code @@ -210,7 +210,7 @@ sudo ./build_helpers/install_ta-lib.sh ##### TA-Lib manual installation -Official webpage: https://mrjbq7.github.io/ta-lib/install.html +[Official installation guide](https://ta-lib.github.io/ta-lib-python/install.html) ```bash wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz diff --git a/docs/producer-consumer.md b/docs/producer-consumer.md index c52279f26..0bd52ac93 100644 --- a/docs/producer-consumer.md +++ b/docs/producer-consumer.md @@ -49,7 +49,7 @@ Enable subscribing to an instance by adding the `external_message_consumer` sect | `wait_timeout` | Timeout until we ping again if no message is received.
*Defaults to `300`.*
**Datatype:** Integer - in seconds. | `ping_timeout` | Ping timeout
*Defaults to `10`.*
**Datatype:** Integer - in seconds. | `sleep_time` | Sleep time before retrying to connect.
*Defaults to `10`.*
**Datatype:** Integer - in seconds. -| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `False`.*
**Datatype:** Boolean. +| `remove_entry_exit_signals` | Remove signal columns from the dataframe (set them to 0) on dataframe receipt.
*Defaults to `false`.*
**Datatype:** Boolean. | `message_size_limit` | Size limit per message
*Defaults to `8`.*
**Datatype:** Integer - Megabytes. Instead of (or as well as) calculating indicators in `populate_indicators()` the follower instance listens on the connection to a producer instance's messages (or multiple producer instances in advanced configurations) and requests the producer's most recently analyzed dataframes for each pair in the active whitelist. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 7f4215aef..91b0e993b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.4 +mkdocs-material==9.1.7 mdx_truly_sane_lists==1.3 -pymdown-extensions==9.10 +pymdown-extensions==9.11 jinja2==3.1.2 diff --git a/docs/rest-api.md b/docs/rest-api.md index 5f604ef43..860a44499 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -9,9 +9,6 @@ This same command can also be used to update freqUI, should there be a new relea Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`). -!!! info "Alpha release" - FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose). - !!! Note "developers" Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. diff --git a/docs/stoploss.md b/docs/stoploss.md index 7af717955..8fc73be21 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -23,10 +23,22 @@ These modes can be configured with these values: 'stoploss_on_exchange_limit_ratio': 0.99 ``` -!!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), Gate (stop-limit), and Kucoin (stop-limit and stop-market) as of now. - Do not set too low/tight stoploss value if using stop loss on exchange! - If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. +Stoploss on exchange is only supported for the following exchanges, and not all exchanges support both stop-limit and stop-market. +The Order-type will be ignored if only one mode is available. + +| Exchange | stop-loss type | +|----------|-------------| +| Binance | limit | +| Binance Futures | market, limit | +| Huobi | limit | +| kraken | market, limit | +| Gate | limit | +| Okx | limit | +| Kucoin | stop-limit, stop-market| + +!!! Note "Tight stoploss" + Do not set too low/tight stoploss value when using stop loss on exchange! + If set to low/tight you will have greater risk of missing fill on the order and stoploss will not work. ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio @@ -197,11 +209,6 @@ You can also keep a static stoploss until the offset is reached, and then trail If `trailing_only_offset_is_reached = True` then the trailing stoploss is only activated once the offset is reached. Until then, the stoploss remains at the configured `stoploss`. This option can be used with or without `trailing_stop_positive`, but uses `trailing_stop_positive_offset` as offset. -``` python - trailing_stop_positive_offset = 0.011 - trailing_only_offset_is_reached = True -``` - Configuration (offset is buy-price + 3%): ``` python diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index cbb71e810..a93dcecdf 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -1,21 +1,21 @@ # Advanced Strategies This page explains some advanced concepts available for strategies. -If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first. +If you're just getting started, please familiarize yourself with the [Freqtrade basics](bot-basics.md) and methods described in [Strategy Customization](strategy-customization.md) first. -[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs. +The call sequence of the methods described here is covered under [bot execution logic](bot-basics.md#bot-execution-logic). Those docs are also helpful in deciding which method is most suitable for your customisation needs. !!! Note - All callback methods described below should only be implemented in a strategy if they are actually used. + Callback methods should *only* be implemented if a strategy uses them. !!! Tip - You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` + Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` ## Storing information Storing information can be accomplished by creating a new dictionary within the strategy class. -The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. +The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. ```python class AwesomeStrategy(IStrategy): diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 329908527..855f2353b 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy): if self.config['runmode'].value in ('live', 'dry_run'): # Assign this to the class by using self.* # can then be used by populate_* methods - self.cust_remote_data = requests.get('https://some_remote_source.example.com') + self.custom_remote_data = requests.get('https://some_remote_source.example.com') ``` @@ -352,7 +352,7 @@ class AwesomeStrategy(IStrategy): # Convert absolute price to percentage relative to current_rate if stoploss_price < current_rate: - return (stoploss_price / current_rate) - 1 + return stoploss_from_absolute(stoploss_price, current_rate, is_short=trade.is_short) # return maximum stoploss value, keeping current stoploss price unchanged return 1 diff --git a/docs/strategy_migration.md b/docs/strategy_migration.md index 22e3d2c22..5ef7a5a4c 100644 --- a/docs/strategy_migration.md +++ b/docs/strategy_migration.md @@ -578,7 +578,7 @@ def populate_any_indicators( Features will now expand automatically. As such, the expansion loops, as well as the `{pair}` / `{timeframe}` parts will need to be removed. ``` python linenums="1" - def feature_engineering_expand_all(self, dataframe, period, **kwargs): + def feature_engineering_expand_all(self, dataframe, period, **kwargs) -> DataFrame:: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -638,7 +638,7 @@ Features will now expand automatically. As such, the expansion loops, as well as Basic features. Make sure to remove the `{pair}` part from your features. ``` python linenums="1" - def feature_engineering_expand_basic(self, dataframe, **kwargs): + def feature_engineering_expand_basic(self, dataframe: DataFrame, **kwargs) -> DataFrame:: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -673,7 +673,7 @@ Basic features. Make sure to remove the `{pair}` part from your features. ### FreqAI - feature engineering standard ``` python linenums="1" - def feature_engineering_standard(self, dataframe, **kwargs): + def feature_engineering_standard(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -704,7 +704,7 @@ Basic features. Make sure to remove the `{pair}` part from your features. Targets now get their own, dedicated method. ``` python linenums="1" - def set_freqai_targets(self, dataframe, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index dc0ab0976..fe990790a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -279,6 +279,7 @@ Return a summary of your profit/loss and performance. > ∙ `33.095 EUR` > > **Total Trade Count:** `138` +> **Bot started:** `2022-07-11 18:40:44` > **First Trade opened:** `3 days ago` > **Latest Trade opened:** `2 minutes ago` > **Avg. Duration:** `2:33:45` @@ -292,6 +293,7 @@ The relative profit of `15.2 Σ%` is be based on the starting capital - so in th Starting capital is either taken from the `available_capital` setting, or calculated by using current wallet size - profits. Profit Factor is calculated as gross profits / gross losses - and should serve as an overall metric for the strategy. Max drawdown corresponds to the backtesting metric `Absolute Drawdown (Account)` - calculated as `(Absolute Drawdown) / (DrawdownHigh + startingBalance)`. +Bot started date will refer to the date the bot was first started. For older bots, this will default to the first trade's open date. ### /forceexit diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 43d6728ee..0327f21e5 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -24,9 +24,9 @@ git clone https://github.com/freqtrade/freqtrade.git Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). -As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial pre-compiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which need to be downloaded and installed using `pip install TA_Lib-0.4.25-cp38-cp38-win_amd64.whl` (make sure to use the version matching your python version). +As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), Freqtrade provides these dependencies (in the binary wheel format) for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows. +These Wheels are also used by CI running on windows, and are therefore tested together with freqtrade. -Freqtrade provides these dependencies for the latest 3 Python versions (3.8, 3.9, 3.10 and 3.11) and for 64bit Windows. Other versions must be downloaded from the above link. ``` powershell @@ -45,8 +45,6 @@ freqtrade The above installation script assumes you're using powershell on a 64bit windows. Commands for the legacy CMD windows console may differ. -> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) - ### Error during installation on Windows ``` bash diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index f56328674..f190e7204 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2023.3' +__version__ = '2023.4' if 'dev' in __version__: from pathlib import Path diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 47aa37fdf..109516f87 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -46,7 +46,7 @@ ARGS_LIST_FREQAIMODELS = ["freqaimodel_path", "print_one_column", "print_coloriz ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] -ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list"] +ARGS_BACKTEST_SHOW = ["exportfilename", "backtest_show_pair_list", "backtest_breakdown"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index adc5e65df..0c2f0d1b8 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -116,7 +116,7 @@ class TimeRange: :param text: value from --timerange :return: Start and End range period """ - if text is None: + if not text: return TimeRange(None, None, 0, 0) syntax = [(r'^-(\d{8})$', (None, 'date')), (r'^(\d{8})-$', ('date', None)), diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ebb946221..b8e240419 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -64,6 +64,7 @@ USERPATH_FREQAIMODELS = 'freqaimodels' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] FULL_DATAFRAME_THRESHOLD = 100 +CUSTOM_TAG_MAX_LENGTH = 255 ENV_VAR_PREFIX = 'FREQTRADE__' @@ -598,7 +599,8 @@ CONF_SCHEMA = { "model_type": {"type": "string", "default": "PPO"}, "policy_type": {"type": "string", "default": "MlpPolicy"}, "net_arch": {"type": "array", "default": [128, 128]}, - "randomize_startinng_position": {"type": "boolean", "default": False}, + "randomize_starting_position": {"type": "boolean", "default": False}, + "progress_bar": {"type": "boolean", "default": True}, "model_reward_parameters": { "type": "object", "properties": { diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 3567f4112..c5905acde 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -246,14 +246,8 @@ def _load_backtest_data_df_compatibility(df: pd.DataFrame) -> pd.DataFrame: """ Compatibility support for older backtest data. """ - df['open_date'] = pd.to_datetime(df['open_date'], - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - utc=True, - infer_datetime_format=True - ) + df['open_date'] = pd.to_datetime(df['open_date'], utc=True) + df['close_date'] = pd.to_datetime(df['close_date'], utc=True) # Compatibility support for pre short Columns if 'is_short' not in df.columns: df['is_short'] = False diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 7ce98de42..2d3855d87 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -34,7 +34,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *, cols = DEFAULT_DATAFRAME_COLUMNS df = DataFrame(ohlcv, columns=cols) - df['date'] = to_datetime(df['date'], unit='ms', utc=True, infer_datetime_format=True) + df['date'] = to_datetime(df['date'], unit='ms', utc=True) # Some exchanges return int values for Volume and even for OHLC. # Convert them since TA-LIB indicators used in the strategy assume floats diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/featherdatahandler.py index bb387fc84..28a12fb29 100644 --- a/freqtrade/data/history/featherdatahandler.py +++ b/freqtrade/data/history/featherdatahandler.py @@ -63,10 +63,7 @@ class FeatherDataHandler(IDataHandler): pairdata.columns = self._columns pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index f016c0ec1..ed7a33f8e 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -75,10 +75,7 @@ class JsonDataHandler(IDataHandler): return DataFrame(columns=self._columns) pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( diff --git a/freqtrade/data/history/parquetdatahandler.py b/freqtrade/data/history/parquetdatahandler.py index 57581861d..e6b2481d2 100644 --- a/freqtrade/data/history/parquetdatahandler.py +++ b/freqtrade/data/history/parquetdatahandler.py @@ -62,10 +62,7 @@ class ParquetDataHandler(IDataHandler): pairdata.columns = self._columns pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float', 'low': 'float', 'close': 'float', 'volume': 'float'}) - pairdata['date'] = to_datetime(pairdata['date'], - unit='ms', - utc=True, - infer_datetime_format=True) + pairdata['date'] = to_datetime(pairdata['date'], unit='ms', utc=True) return pairdata def ohlcv_append( diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index b815fb3ee..8092d5af8 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -6,17 +6,18 @@ from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.binance import Binance from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bittrex import Bittrex +from freqtrade.exchange.bitvavo import Bitvavo from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.coinbasepro import Coinbasepro -from freqtrade.exchange.exchange_utils import (amount_to_contract_precision, amount_to_contracts, - amount_to_precision, available_exchanges, - ccxt_exchanges, contracts_to_amount, - date_minus_candles, is_exchange_known_ccxt, - market_is_active, price_to_precision, - timeframe_to_minutes, timeframe_to_msecs, - timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds, validate_exchange, - validate_exchanges) +from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, + amount_to_contracts, amount_to_precision, + available_exchanges, ccxt_exchanges, + contracts_to_amount, date_minus_candles, + is_exchange_known_ccxt, market_is_active, + price_to_precision, timeframe_to_minutes, + timeframe_to_msecs, timeframe_to_next_date, + timeframe_to_prev_date, timeframe_to_seconds, + validate_exchange, validate_exchanges) from freqtrade.exchange.gate import Gate from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.huobi import Huobi diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 597db27ff..0b9be0f55 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -1743,6 +1743,120 @@ } } ], + "AMB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "ANC/BUSD:BUSD": [ { "tier": 1.0, @@ -3526,10 +3640,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -3542,10 +3656,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "15", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -3558,10 +3672,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "10", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -3620,13 +3734,13 @@ "tier": 7.0, "currency": "BUSD", "minNotional": 3000000.0, - "maxNotional": 8000000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "8000000", + "notionalCap": "4000000", "notionalFloor": "3000000", "maintMarginRatio": "0.5", "cum": "949400.0" @@ -5487,120 +5601,6 @@ } } ], - "BTC/USDT:USDT-230331": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 375000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "375000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 375000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "2000000", - "notionalFloor": "375000", - "maintMarginRatio": "0.05", - "cum": "11250.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "4000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.1", - "cum": "111250.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 4000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "4", - "initialLeverage": "4", - "notionalCap": "10000000", - "notionalFloor": "4000000", - "maintMarginRatio": "0.125", - "cum": "211250.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "5", - "initialLeverage": "3", - "notionalCap": "20000000", - "notionalFloor": "10000000", - "maintMarginRatio": "0.15", - "cum": "461250.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 40000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "40000000", - "notionalFloor": "20000000", - "maintMarginRatio": "0.25", - "cum": "2461250.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 40000000.0, - "maxNotional": 400000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "400000000", - "notionalFloor": "40000000", - "maintMarginRatio": "0.5", - "cum": "1.246125E7" - } - } - ], "BTC/USDT:USDT-230630": [ { "tier": 1.0, @@ -6016,10 +6016,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -6032,10 +6032,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -6046,13 +6046,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -6061,33 +6061,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "35650.0" } }, { @@ -6095,15 +6095,31 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "910650.0" } } ], @@ -6114,10 +6130,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 25.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -6130,10 +6146,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -6144,13 +6160,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 300000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "8", - "notionalCap": "100000", + "initialLeverage": "10", + "notionalCap": "300000", "notionalFloor": "25000", "maintMarginRatio": "0.05", "cum": "650.0" @@ -6159,33 +6175,33 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 300000.0, + "maxNotional": 800000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "800000", + "notionalFloor": "300000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "15650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 800000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "800000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "35650.0" } }, { @@ -6193,15 +6209,31 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "160650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "910650.0" } } ], @@ -10097,120 +10129,6 @@ } } ], - "ETH/USDT:USDT-230331": [ - { - "tier": 1.0, - "currency": "USDT", - "minNotional": 0.0, - "maxNotional": 375000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, - "info": { - "bracket": "1", - "initialLeverage": "25", - "notionalCap": "375000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "USDT", - "minNotional": 375000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "2000000", - "notionalFloor": "375000", - "maintMarginRatio": "0.05", - "cum": "11250.0" - } - }, - { - "tier": 3.0, - "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "4000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.1", - "cum": "111250.0" - } - }, - { - "tier": 4.0, - "currency": "USDT", - "minNotional": 4000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "4", - "initialLeverage": "4", - "notionalCap": "10000000", - "notionalFloor": "4000000", - "maintMarginRatio": "0.125", - "cum": "211250.0" - } - }, - { - "tier": 5.0, - "currency": "USDT", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "5", - "initialLeverage": "3", - "notionalCap": "20000000", - "notionalFloor": "10000000", - "maintMarginRatio": "0.15", - "cum": "461250.0" - } - }, - { - "tier": 6.0, - "currency": "USDT", - "minNotional": 20000000.0, - "maxNotional": 40000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "40000000", - "notionalFloor": "20000000", - "maintMarginRatio": "0.25", - "cum": "2461250.0" - } - }, - { - "tier": 7.0, - "currency": "USDT", - "minNotional": 40000000.0, - "maxNotional": 400000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "400000000", - "notionalFloor": "40000000", - "maintMarginRatio": "0.5", - "cum": "1.246125E7" - } - } - ], "ETH/USDT:USDT-230630": [ { "tier": 1.0, @@ -11930,10 +11848,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -11946,10 +11864,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -11962,10 +11880,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -12008,13 +11926,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -12559,6 +12477,120 @@ } } ], + "HFT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "HIGH/USDT:USDT": [ { "tier": 1.0, @@ -13945,6 +13977,120 @@ } } ], + "JOE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "KAVA/USDT:USDT": [ { "tier": 1.0, @@ -14682,19 +14828,133 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "2000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386885.0" } } ], + "LEVER/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "LINA/USDT:USDT": [ { "tier": 1.0, @@ -16850,10 +17110,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -16866,10 +17126,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -16882,10 +17142,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -16928,13 +17188,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "3000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -18154,10 +18414,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 11.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "11", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -18170,10 +18430,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -18186,10 +18446,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -18232,13 +18492,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -18653,6 +18913,120 @@ } } ], + "RDNT/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "REEF/USDT:USDT": [ { "tier": 1.0, @@ -19508,10 +19882,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -19524,10 +19898,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -19540,10 +19914,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -19586,13 +19960,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -21251,14 +21625,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 15.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "15", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -21268,78 +21642,94 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 10.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "10", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 600000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxLeverage": 8.0, "info": { "bracket": "3", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "8", + "notionalCap": "600000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 600000.0, + "maxNotional": 1600000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1600000", + "notionalFloor": "600000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "30650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1600000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1600000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "70650.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "320650.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 6500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalCap": "6500000", + "notionalFloor": "6000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "1820650.0" } } ], @@ -21578,10 +21968,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -21594,10 +21984,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -21610,10 +22000,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -21656,13 +22046,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -21675,14 +22065,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 25.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "1", - "initialLeverage": "25", + "initialLeverage": "20", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.01", + "maintMarginRatio": "0.02", "cum": "0.0" } }, @@ -21692,78 +22082,94 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maxLeverage": 15.0, "info": { "bracket": "2", - "initialLeverage": "20", + "initialLeverage": "15", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "75.0" + "cum": "25.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 25000.0, - "maxNotional": 100000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", + "notionalCap": "200000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "700.0" + "cum": "650.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5700.0" + "cum": "10650.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11950.0" + "cum": "23150.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "386950.0" + "cum": "898150.0" } } ], @@ -22458,10 +22864,10 @@ "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "maxLeverage": 8.0, "info": { "bracket": "1", - "initialLeverage": "20", + "initialLeverage": "8", "notionalCap": "5000", "notionalFloor": "0", "maintMarginRatio": "0.02", @@ -22474,10 +22880,10 @@ "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxLeverage": 7.0, "info": { "bracket": "2", - "initialLeverage": "10", + "initialLeverage": "7", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", @@ -22490,10 +22896,10 @@ "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "maxLeverage": 6.0, "info": { "bracket": "3", - "initialLeverage": "8", + "initialLeverage": "6", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", @@ -22536,13 +22942,13 @@ "tier": 6.0, "currency": "BUSD", "minNotional": 1000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5000000", + "notionalCap": "1500000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", "cum": "386900.0" @@ -23965,6 +24371,120 @@ } } ], + "XVS/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, + "info": { + "bracket": "1", + "initialLeverage": "20", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.02", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 15.0, + "info": { + "bracket": "2", + "initialLeverage": "15", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "25.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "650.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.1", + "cum": "10650.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.125", + "cum": "23150.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148150.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 3000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "5000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.5", + "cum": "898150.0" + } + } + ], "YFI/USDT:USDT": [ { "tier": 1.0, diff --git a/freqtrade/exchange/bitvavo.py b/freqtrade/exchange/bitvavo.py new file mode 100644 index 000000000..ba1d355cc --- /dev/null +++ b/freqtrade/exchange/bitvavo.py @@ -0,0 +1,23 @@ +"""Kucoin exchange subclass.""" +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bitvavo(Exchange): + """Bitvavo exchange class. + + Contains adjustments needed for Freqtrade to work with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 1440, + } diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bbe9585ae..7e276d538 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -30,13 +30,14 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun RetryableOrderError, TemporaryError) from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier, retrier_async) -from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision, - amount_to_contracts, amount_to_precision, - contracts_to_amount, date_minus_candles, - is_exchange_known_ccxt, market_is_active, - price_to_precision, timeframe_to_minutes, - timeframe_to_msecs, timeframe_to_next_date, - timeframe_to_prev_date, timeframe_to_seconds) +from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType, + amount_to_contract_precision, amount_to_contracts, + amount_to_precision, contracts_to_amount, + date_minus_candles, is_exchange_known_ccxt, + market_is_active, price_to_precision, + timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, timeframe_to_prev_date, + timeframe_to_seconds) from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json, safe_value_fallback2) @@ -59,6 +60,7 @@ class Exchange: # or by specifying them in the configuration. _ft_has_default: Dict = { "stoploss_on_exchange": False, + "stop_price_param": "stopPrice", "order_time_in_force": ["GTC"], "ohlcv_params": {}, "ohlcv_candle_limit": 500, @@ -734,12 +736,14 @@ class Exchange: """ return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode) - def price_to_precision(self, pair: str, price: float) -> float: + def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float: """ - Returns the price rounded up to the precision the Exchange accepts. - Rounds up + Returns the price rounded to the precision the Exchange accepts. + The default price_rounding_mode in conf is ROUND. + For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts. """ - return price_to_precision(price, self.get_precision_price(pair), self.precisionMode) + return price_to_precision(price, self.get_precision_price(pair), + self.precisionMode, rounding_mode=rounding_mode) def price_get_one_pip(self, pair: str, price: float) -> float: """ @@ -762,12 +766,12 @@ class Exchange: return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage) def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float: - max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max') + max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage) if max_stake_amount is None: # * Should never be executed raise OperationalException(f'{self.name}.get_max_pair_stake_amount should' 'never set max_stake_amount to None') - return max_stake_amount / leverage + return max_stake_amount def _get_stake_amount_limit( self, @@ -785,43 +789,41 @@ class Exchange: except KeyError: raise ValueError(f"Can't get market information for symbol {pair}") + if isMin: + # reserve some percent defined in config (5% default) + stoploss + margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent', + DEFAULT_AMOUNT_RESERVE_PERCENT) + stoploss_reserve = ( + margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + ) + # it should not be more than 50% + stoploss_reserve = max(min(stoploss_reserve, 1.5), 1) + else: + margin_reserve = 1.0 + stoploss_reserve = 1.0 + stake_limits = [] limits = market['limits'] if (limits['cost'][limit] is not None): stake_limits.append( - self._contracts_to_amount( - pair, - limits['cost'][limit] - ) + self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve ) if (limits['amount'][limit] is not None): stake_limits.append( - self._contracts_to_amount( - pair, - limits['amount'][limit] * price - ) + self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve ) if not stake_limits: return None if isMin else float('inf') - # reserve some percent defined in config (5% default) + stoploss - amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', - DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent = ( - amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 - ) - # it should not be more than 50% - amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) - # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. return self._get_stake_amount_considering_leverage( - max(stake_limits) * amount_reserve_percent, + max(stake_limits) if isMin else min(stake_limits), leverage or 1.0 - ) if isMin else min(stake_limits) + ) def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float: """ @@ -884,7 +886,7 @@ class Exchange: 'filled': _amount, 'remaining': 0.0, 'status': "closed", - 'cost': (dry_order['amount'] * average) / leverage + 'cost': (dry_order['amount'] * average) }) # market orders will always incurr taker fees dry_order = self.add_dry_order_fee(pair, dry_order, 'taker') @@ -1114,11 +1116,11 @@ class Exchange: """ if not self._ft_has.get('stoploss_on_exchange'): raise OperationalException(f"stoploss is not implemented for {self.name}.") - + price_param = self._ft_has['stop_price_param'] return ( - order.get('stopPrice', None) is None - or ((side == "sell" and stop_loss > float(order['stopPrice'])) or - (side == "buy" and stop_loss < float(order['stopPrice']))) + order.get(price_param, None) is None + or ((side == "sell" and stop_loss > float(order[price_param])) or + (side == "buy" and stop_loss < float(order[price_param]))) ) def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]: @@ -1158,8 +1160,8 @@ class Exchange: def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: params = self._params.copy() - # Verify if stopPrice works for your exchange! - params.update({'stopPrice': stop_price}) + # Verify if stopPrice works for your exchange, else configure stop_price_param + params.update({self._ft_has['stop_price_param']: stop_price}) return params @retrier(retries=0) @@ -1185,12 +1187,12 @@ class Exchange: user_order_type = order_types.get('stoploss', 'market') ordertype, user_order_type = self._get_stop_order_type(user_order_type) - - stop_price_norm = self.price_to_precision(pair, stop_price) + round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP + stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode) limit_rate = None if user_order_type == 'limit': limit_rate = self._get_stop_limit_rate(stop_price, order_types, side) - limit_rate = self.price_to_precision(pair, limit_rate) + limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode) if self._config['dry_run']: dry_order = self.create_dry_run_order( @@ -2369,12 +2371,12 @@ class Exchange: # Must fetch the leverage tiers for each market separately # * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets markets = self.markets - symbols = [] - for symbol, market in markets.items(): + symbols = [ + symbol for symbol, market in markets.items() if (self.market_is_future(market) - and market['quote'] == self._config['stake_currency']): - symbols.append(symbol) + and market['quote'] == self._config['stake_currency']) + ] tiers: Dict[str, List[Dict]] = {} @@ -2394,25 +2396,26 @@ class Exchange: else: logger.info("Using cached leverage_tiers.") - async def gather_results(): + async def gather_results(input_coro): return await asyncio.gather(*input_coro, return_exceptions=True) for input_coro in chunks(coros, 100): with self._loop_lock: - results = self.loop.run_until_complete(gather_results()) + results = self.loop.run_until_complete(gather_results(input_coro)) - for symbol, res in results: - tiers[symbol] = res + for res in results: + if isinstance(res, Exception): + logger.warning(f"Leverage tier exception: {repr(res)}") + continue + symbol, tier = res + tiers[symbol] = tier if len(coros) > 0: self.cache_leverage_tiers(tiers, self._config['stake_currency']) logger.info(f"Done initializing {len(symbols)} markets.") return tiers - else: - return {} - else: - return {} + return {} def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None: @@ -2428,14 +2431,17 @@ class Exchange: def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]: filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json" if filename.is_file(): - tiers = file_load_json(filename) - updated = tiers.get('updated') - if updated: - updated_dt = parser.parse(updated) - if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4): - logger.info("Cached leverage tiers are outdated. Will update.") - return None - return tiers['data'] + try: + tiers = file_load_json(filename) + updated = tiers.get('updated') + if updated: + updated_dt = parser.parse(updated) + if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4): + logger.info("Cached leverage tiers are outdated. Will update.") + return None + return tiers['data'] + except Exception: + logger.exception("Error loading cached leverage tiers. Refreshing.") return None def fill_leverage_tiers(self) -> None: diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index 6d3371a59..83d2a214d 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -2,11 +2,12 @@ Exchange support utils """ from datetime import datetime, timedelta, timezone -from math import ceil +from math import ceil, floor from typing import Any, Dict, List, Optional, Tuple import ccxt -from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision +from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE, + TRUNCATE, decimal_to_precision) from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED from freqtrade.util import FtPrecise @@ -219,35 +220,51 @@ def amount_to_contract_precision( return amount -def price_to_precision(price: float, price_precision: Optional[float], - precisionMode: Optional[int]) -> float: +def price_to_precision( + price: float, + price_precision: Optional[float], + precisionMode: Optional[int], + *, + rounding_mode: int = ROUND, +) -> float: """ - Returns the price rounded up to the precision the Exchange accepts. + Returns the price rounded to the precision the Exchange accepts. Partial Re-implementation of ccxt internal method decimal_to_precision(), - which does not support rounding up + which does not support rounding up. + For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts. + TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and align with amount_to_precision(). - !!! Rounds up :param price: price to convert :param price_precision: price precision to use. Used from markets[pair]['precision']['price'] :param precisionMode: precision mode to use. Should be used from precisionMode one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE + :param rounding_mode: rounding mode to use. Defaults to ROUND :return: price rounded up to the precision the Exchange accepts - """ if price_precision is not None and precisionMode is not None: - # price = float(decimal_to_precision(price, rounding_mode=ROUND, - # precision=price_precision, - # counting_mode=self.precisionMode, - # )) if precisionMode == TICK_SIZE: + if rounding_mode == ROUND: + ticks = price / price_precision + rounded_ticks = round(ticks) + return rounded_ticks * price_precision precision = FtPrecise(price_precision) price_str = FtPrecise(price) missing = price_str % precision if not missing == FtPrecise("0"): - price = round(float(str(price_str - missing + precision)), 14) - else: - symbol_prec = price_precision - big_price = price * pow(10, symbol_prec) - price = ceil(big_price) / pow(10, symbol_prec) + return round(float(str(price_str - missing + precision)), 14) + return price + elif precisionMode in (SIGNIFICANT_DIGITS, DECIMAL_PLACES): + ndigits = round(price_precision) + if rounding_mode == ROUND: + return round(price, ndigits) + ticks = price * (10**ndigits) + if rounding_mode == ROUND_UP: + return ceil(ticks) / (10**ndigits) + if rounding_mode == TRUNCATE: + return int(ticks) / (10**ndigits) + if rounding_mode == ROUND_DOWN: + return floor(ticks) / (10**ndigits) + raise ValueError(f"Unknown rounding_mode {rounding_mode}") + raise ValueError(f"Unknown precisionMode {precisionMode}") return price diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index b1a19fa69..c41bb6d56 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -12,6 +12,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, Invali OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier +from freqtrade.exchange.exchange_utils import ROUND_DOWN, ROUND_UP from freqtrade.exchange.types import Tickers @@ -109,6 +110,7 @@ class Kraken(Exchange): if self.trading_mode == TradingMode.FUTURES: params.update({'reduceOnly': True}) + round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP if order_types.get('stoploss', 'market') == 'limit': ordertype = "stop-loss-limit" limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) @@ -116,11 +118,11 @@ class Kraken(Exchange): limit_rate = stop_price * limit_price_pct else: limit_rate = stop_price * (2 - limit_price_pct) - params['price2'] = self.price_to_precision(pair, limit_rate) + params['price2'] = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode) else: ordertype = "stop-loss" - stop_price = self.price_to_precision(pair, stop_price) + stop_price = self.price_to_precision(pair, stop_price, rounding_mode=round_mode) if self._config['dry_run']: dry_order = self.create_dry_run_order( diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index a4fcaeca0..84b7deb7a 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -28,6 +28,7 @@ class Okx(Exchange): "funding_fee_timeframe": "8h", "stoploss_order_types": {"limit": "limit"}, "stoploss_on_exchange": True, + "stop_price_param": "stopLossPrice", } _ft_has_futures: Dict = { "tickers_have_quoteVolume": False, @@ -162,29 +163,12 @@ class Okx(Exchange): return pair_tiers[-1]['maxNotional'] / leverage def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: - - params = self._params.copy() - # Verify if stopPrice works for your exchange! - params.update({'stopLossPrice': stop_price}) - + params = super()._get_stop_params(side, ordertype, stop_price) if self.trading_mode == TradingMode.FUTURES and self.margin_mode: params['tdMode'] = self.margin_mode.value params['posSide'] = self._get_posSide(side, True) return params - def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: - """ - OKX uses non-default stoploss price naming. - """ - if not self._ft_has.get('stoploss_on_exchange'): - raise OperationalException(f"stoploss is not implemented for {self.name}.") - - return ( - order.get('stopLossPrice', None) is None - or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or - (side == "buy" and stop_loss < float(order['stopLossPrice']))) - ) - def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) diff --git a/freqtrade/freqai/RL/Base3ActionRLEnv.py b/freqtrade/freqai/RL/Base3ActionRLEnv.py index a108d776e..c0a7eedaa 100644 --- a/freqtrade/freqai/RL/Base3ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base3ActionRLEnv.py @@ -66,7 +66,7 @@ class Base3ActionRLEnv(BaseEnvironment): elif action == Actions.Sell.value and not self.can_short: self._update_total_profit() self._position = Positions.Neutral - trade_type = "neutral" + trade_type = "exit" self._last_trade_tick = None else: print("case not defined") @@ -74,7 +74,7 @@ class Base3ActionRLEnv(BaseEnvironment): if trade_type is not None: self.trade_history.append( {'price': self.current_price(), 'index': self._current_tick, - 'type': trade_type}) + 'type': trade_type, 'profit': self.get_unrealized_profit()}) if (self._total_profit < self.max_drawdown or self._total_unrealized_profit < self.max_drawdown): diff --git a/freqtrade/freqai/RL/Base4ActionRLEnv.py b/freqtrade/freqai/RL/Base4ActionRLEnv.py index 4f093f06c..e883136b2 100644 --- a/freqtrade/freqai/RL/Base4ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base4ActionRLEnv.py @@ -52,16 +52,6 @@ class Base4ActionRLEnv(BaseEnvironment): trade_type = None if self.is_tradesignal(action): - """ - Action: Neutral, position: Long -> Close Long - Action: Neutral, position: Short -> Close Short - - Action: Long, position: Neutral -> Open Long - Action: Long, position: Short -> Close Short and Open Long - - Action: Short, position: Neutral -> Open Short - Action: Short, position: Long -> Close Long and Open Short - """ if action == Actions.Neutral.value: self._position = Positions.Neutral @@ -69,16 +59,16 @@ class Base4ActionRLEnv(BaseEnvironment): self._last_trade_tick = None elif action == Actions.Long_enter.value: self._position = Positions.Long - trade_type = "long" + trade_type = "enter_long" self._last_trade_tick = self._current_tick elif action == Actions.Short_enter.value: self._position = Positions.Short - trade_type = "short" + trade_type = "enter_short" self._last_trade_tick = self._current_tick elif action == Actions.Exit.value: self._update_total_profit() self._position = Positions.Neutral - trade_type = "neutral" + trade_type = "exit" self._last_trade_tick = None else: print("case not defined") @@ -86,7 +76,7 @@ class Base4ActionRLEnv(BaseEnvironment): if trade_type is not None: self.trade_history.append( {'price': self.current_price(), 'index': self._current_tick, - 'type': trade_type}) + 'type': trade_type, 'profit': self.get_unrealized_profit()}) if (self._total_profit < self.max_drawdown or self._total_unrealized_profit < self.max_drawdown): diff --git a/freqtrade/freqai/RL/Base5ActionRLEnv.py b/freqtrade/freqai/RL/Base5ActionRLEnv.py index 490ef3601..816211cc2 100644 --- a/freqtrade/freqai/RL/Base5ActionRLEnv.py +++ b/freqtrade/freqai/RL/Base5ActionRLEnv.py @@ -53,16 +53,6 @@ class Base5ActionRLEnv(BaseEnvironment): trade_type = None if self.is_tradesignal(action): - """ - Action: Neutral, position: Long -> Close Long - Action: Neutral, position: Short -> Close Short - - Action: Long, position: Neutral -> Open Long - Action: Long, position: Short -> Close Short and Open Long - - Action: Short, position: Neutral -> Open Short - Action: Short, position: Long -> Close Long and Open Short - """ if action == Actions.Neutral.value: self._position = Positions.Neutral @@ -70,21 +60,21 @@ class Base5ActionRLEnv(BaseEnvironment): self._last_trade_tick = None elif action == Actions.Long_enter.value: self._position = Positions.Long - trade_type = "long" + trade_type = "enter_long" self._last_trade_tick = self._current_tick elif action == Actions.Short_enter.value: self._position = Positions.Short - trade_type = "short" + trade_type = "enter_short" self._last_trade_tick = self._current_tick elif action == Actions.Long_exit.value: self._update_total_profit() self._position = Positions.Neutral - trade_type = "neutral" + trade_type = "exit_long" self._last_trade_tick = None elif action == Actions.Short_exit.value: self._update_total_profit() self._position = Positions.Neutral - trade_type = "neutral" + trade_type = "exit_short" self._last_trade_tick = None else: print("case not defined") @@ -92,7 +82,7 @@ class Base5ActionRLEnv(BaseEnvironment): if trade_type is not None: self.trade_history.append( {'price': self.current_price(), 'index': self._current_tick, - 'type': trade_type}) + 'type': trade_type, 'profit': self.get_unrealized_profit()}) if (self._total_profit < self.max_drawdown or self._total_unrealized_profit < self.max_drawdown): diff --git a/freqtrade/freqai/base_models/BasePyTorchClassifier.py b/freqtrade/freqai/base_models/BasePyTorchClassifier.py new file mode 100644 index 000000000..977152cc5 --- /dev/null +++ b/freqtrade/freqai/base_models/BasePyTorchClassifier.py @@ -0,0 +1,147 @@ +import logging +from typing import Dict, List, Tuple + +import numpy as np +import numpy.typing as npt +import pandas as pd +import torch +from pandas import DataFrame +from torch.nn import functional as F + +from freqtrade.exceptions import OperationalException +from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + + +logger = logging.getLogger(__name__) + + +class BasePyTorchClassifier(BasePyTorchModel): + """ + A PyTorch implementation of a classifier. + User must implement fit method + + Important! + + - User must declare the target class names in the strategy, + under IStrategy.set_freqai_targets method. + + for example, in your strategy: + ``` + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + self.freqai.class_names = ["down", "up"] + dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > + dataframe["close"], 'up', 'down') + + return dataframe + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.class_name_to_index = None + self.index_to_class_name = None + + def predict( + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + :raises ValueError: if 'class_names' doesn't exist in model meta_data. + """ + + class_names = self.model.model_meta_data.get("class_names", None) + if not class_names: + raise ValueError( + "Missing class names. " + "self.model.model_meta_data['class_names'] is None." + ) + + if not self.class_name_to_index: + self.init_class_names_to_index_mapping(class_names) + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + self.data_cleaning_predict(dk) + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) + logits = self.model.model(x) + probs = F.softmax(logits, dim=-1) + predicted_classes = torch.argmax(probs, dim=-1) + predicted_classes_str = self.decode_class_names(predicted_classes) + pred_df_prob = DataFrame(probs.detach().numpy(), columns=class_names) + pred_df = DataFrame(predicted_classes_str, columns=[dk.label_list[0]]) + pred_df = pd.concat([pred_df, pred_df_prob], axis=1) + return (pred_df, dk.do_predict) + + def encode_class_names( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str], + ): + """ + encode class name, str -> int + assuming first column of *_labels data frame to be the target column + containing the class names + """ + + target_column_name = dk.label_list[0] + for split in self.splits: + label_df = data_dictionary[f"{split}_labels"] + self.assert_valid_class_names(label_df[target_column_name], class_names) + label_df[target_column_name] = list( + map(lambda x: self.class_name_to_index[x], label_df[target_column_name]) + ) + + @staticmethod + def assert_valid_class_names( + target_column: pd.Series, + class_names: List[str] + ): + non_defined_labels = set(target_column) - set(class_names) + if len(non_defined_labels) != 0: + raise OperationalException( + f"Found non defined labels: {non_defined_labels}, ", + f"expecting labels: {class_names}" + ) + + def decode_class_names(self, class_ints: torch.Tensor) -> List[str]: + """ + decode class name, int -> str + """ + + return list(map(lambda x: self.index_to_class_name[x.item()], class_ints)) + + def init_class_names_to_index_mapping(self, class_names): + self.class_name_to_index = {s: i for i, s in enumerate(class_names)} + self.index_to_class_name = {i: s for i, s in enumerate(class_names)} + logger.info(f"encoded class name to index: {self.class_name_to_index}") + + def convert_label_column_to_int( + self, + data_dictionary: Dict[str, pd.DataFrame], + dk: FreqaiDataKitchen, + class_names: List[str] + ): + self.init_class_names_to_index_mapping(class_names) + self.encode_class_names(data_dictionary, dk, class_names) + + def get_class_names(self) -> List[str]: + if not self.class_names: + raise ValueError( + "self.class_names is empty, " + "set self.freqai.class_names = ['class a', 'class b', 'class c'] " + "inside IStrategy.set_freqai_targets method." + ) + + return self.class_names diff --git a/freqtrade/freqai/base_models/BasePyTorchModel.py b/freqtrade/freqai/base_models/BasePyTorchModel.py new file mode 100644 index 000000000..8177b8eb8 --- /dev/null +++ b/freqtrade/freqai/base_models/BasePyTorchModel.py @@ -0,0 +1,83 @@ +import logging +from abc import ABC, abstractmethod +from time import time +from typing import Any + +import torch +from pandas import DataFrame + +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.freqai_interface import IFreqaiModel +from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor + + +logger = logging.getLogger(__name__) + + +class BasePyTorchModel(IFreqaiModel, ABC): + """ + Base class for PyTorch type models. + User *must* inherit from this class and set fit() and predict() and + data_convertor property. + """ + + def __init__(self, **kwargs): + super().__init__(config=kwargs["config"]) + self.dd.model_type = "pytorch" + self.device = "cuda" if torch.cuda.is_available() else "cpu" + test_size = self.freqai_info.get('data_split_parameters', {}).get('test_size') + self.splits = ["train", "test"] if test_size != 0 else ["train"] + + def train( + self, unfiltered_df: DataFrame, pair: str, dk: FreqaiDataKitchen, **kwargs + ) -> Any: + """ + Filter the training data and train a model to it. Train makes heavy use of the datakitchen + for storing, saving, loading, and analyzing the data. + :param unfiltered_df: Full dataframe for the current training period + :return: + :model: Trained model which can be used to inference (self.predict) + """ + + logger.info(f"-------------------- Starting training {pair} --------------------") + + start_time = time() + + features_filtered, labels_filtered = dk.filter_features( + unfiltered_df, + dk.training_features_list, + dk.label_list, + training_filter=True, + ) + + # split data into train/test data. + data_dictionary = dk.make_train_test_datasets(features_filtered, labels_filtered) + if not self.freqai_info.get("fit_live_predictions", 0) or not self.live: + dk.fit_labels() + # normalize all data based on train_dataset only + data_dictionary = dk.normalize_data(data_dictionary) + + # optional additional data cleaning/analysis + self.data_cleaning_train(dk) + + logger.info( + f"Training model on {len(dk.data_dictionary['train_features'].columns)} features" + ) + logger.info(f"Training model on {len(data_dictionary['train_features'])} data points") + + model = self.fit(data_dictionary, dk) + end_time = time() + + logger.info(f"-------------------- Done training {pair} " + f"({end_time - start_time:.2f} secs) --------------------") + + return model + + @property + @abstractmethod + def data_convertor(self) -> PyTorchDataConvertor: + """ + a class responsible for converting `*_features` & `*_labels` pandas dataframes + to pytorch tensors. + """ + raise NotImplementedError("Abstract property") diff --git a/freqtrade/freqai/base_models/BasePyTorchRegressor.py b/freqtrade/freqai/base_models/BasePyTorchRegressor.py new file mode 100644 index 000000000..ea6fabe49 --- /dev/null +++ b/freqtrade/freqai/base_models/BasePyTorchRegressor.py @@ -0,0 +1,50 @@ +import logging +from typing import Tuple + +import numpy as np +import numpy.typing as npt +from pandas import DataFrame + +from freqtrade.freqai.base_models.BasePyTorchModel import BasePyTorchModel +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen + + +logger = logging.getLogger(__name__) + + +class BasePyTorchRegressor(BasePyTorchModel): + """ + A PyTorch implementation of a regressor. + User must implement fit method + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def predict( + self, unfiltered_df: DataFrame, dk: FreqaiDataKitchen, **kwargs + ) -> Tuple[DataFrame, npt.NDArray[np.int_]]: + """ + Filter the prediction features data and predict with it. + :param unfiltered_df: Full dataframe for the current backtest period. + :return: + :pred_df: dataframe containing the predictions + :do_predict: np.array of 1s and 0s to indicate places where freqai needed to remove + data (NaNs) or felt uncertain about data (PCA and DI index) + """ + + dk.find_features(unfiltered_df) + filtered_df, _ = dk.filter_features( + unfiltered_df, dk.training_features_list, training_filter=False + ) + filtered_df = dk.normalize_data_from_metadata(filtered_df) + dk.data_dictionary["prediction_features"] = filtered_df + + self.data_cleaning_predict(dk) + x = self.data_convertor.convert_x( + dk.data_dictionary["prediction_features"], + device=self.device + ) + y = self.model.model(x) + y = y.cpu() + pred_df = DataFrame(y.detach().numpy(), columns=[dk.label_list[0]]) + return (pred_df, dk.do_predict) diff --git a/freqtrade/freqai/data_drawer.py b/freqtrade/freqai/data_drawer.py index 14986d854..b68a9dcad 100644 --- a/freqtrade/freqai/data_drawer.py +++ b/freqtrade/freqai/data_drawer.py @@ -446,7 +446,7 @@ class FreqaiDataDrawer: dump(model, save_path / f"{dk.model_filename}_model.joblib") elif self.model_type == 'keras': model.save(save_path / f"{dk.model_filename}_model.h5") - elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type: + elif self.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: model.save(save_path / f"{dk.model_filename}_model.zip") if dk.svm_model is not None: @@ -496,7 +496,7 @@ class FreqaiDataDrawer: dk.training_features_list = dk.data["training_features_list"] dk.label_list = dk.data["label_list"] - def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: + def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any: # noqa: C901 """ loads all data required to make a prediction on a sub-train time range :returns: @@ -537,6 +537,11 @@ class FreqaiDataDrawer: self.model_type, self.freqai_info['rl_config']['model_type']) MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type']) model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model") + elif self.model_type == 'pytorch': + import torch + zip = torch.load(dk.data_path / f"{dk.model_filename}_model.zip") + model = zip["pytrainer"] + model = model.load_from_checkpoint(zip) if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file(): dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib") diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index 52d487b08..21b41db2d 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -1291,7 +1291,7 @@ class FreqaiDataKitchen: return dataframe - def use_strategy_to_populate_indicators( + def use_strategy_to_populate_indicators( # noqa: C901 self, strategy: IStrategy, corr_dataframes: dict = {}, @@ -1362,12 +1362,12 @@ class FreqaiDataKitchen: dataframe = self.populate_features(dataframe.copy(), corr_pair, strategy, corr_dataframes, base_dataframes, True) - dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata) + if self.live: + dataframe = strategy.set_freqai_targets(dataframe.copy(), metadata=metadata) + dataframe = self.remove_special_chars_from_feature_names(dataframe) self.get_unique_classes_from_labels(dataframe) - dataframe = self.remove_special_chars_from_feature_names(dataframe) - if self.config.get('reduce_df_footprint', False): dataframe = reduce_dataframe_footprint(dataframe) diff --git a/freqtrade/freqai/freqai_interface.py b/freqtrade/freqai/freqai_interface.py index b657bd811..7eaaeab3e 100644 --- a/freqtrade/freqai/freqai_interface.py +++ b/freqtrade/freqai/freqai_interface.py @@ -83,6 +83,7 @@ class IFreqaiModel(ABC): self.CONV_WIDTH = self.freqai_info.get('conv_width', 1) if self.ft_params.get("inlier_metric_window", 0): self.CONV_WIDTH = self.ft_params.get("inlier_metric_window", 0) * 2 + self.class_names: List[str] = [] # used in classification subclasses self.pair_it = 0 self.pair_it_train = 0 self.total_pairs = len(self.config.get("exchange", {}).get("pair_whitelist")) @@ -306,7 +307,7 @@ class IFreqaiModel(ABC): if check_features: self.dd.load_metadata(dk) dataframe_dummy_features = self.dk.use_strategy_to_populate_indicators( - strategy, prediction_dataframe=dataframe.tail(1), pair=metadata["pair"] + strategy, prediction_dataframe=dataframe.tail(1), pair=pair ) dk.find_features(dataframe_dummy_features) self.check_if_feature_list_matches_strategy(dk) @@ -316,7 +317,7 @@ class IFreqaiModel(ABC): else: if populate_indicators: dataframe = self.dk.use_strategy_to_populate_indicators( - strategy, prediction_dataframe=dataframe, pair=metadata["pair"] + strategy, prediction_dataframe=dataframe, pair=pair ) populate_indicators = False @@ -332,6 +333,10 @@ class IFreqaiModel(ABC): dataframe_train = dk.slice_dataframe(tr_train, dataframe_base_train) dataframe_backtest = dk.slice_dataframe(tr_backtest, dataframe_base_backtest) + dataframe_train = dk.remove_special_chars_from_feature_names(dataframe_train) + dataframe_backtest = dk.remove_special_chars_from_feature_names(dataframe_backtest) + dk.get_unique_classes_from_labels(dataframe_train) + if not self.model_exists(dk): dk.find_features(dataframe_train) dk.find_labels(dataframe_train) @@ -567,8 +572,9 @@ class IFreqaiModel(ABC): file_type = ".joblib" elif self.dd.model_type == 'keras': file_type = ".h5" - elif 'stable_baselines' in self.dd.model_type or 'sb3_contrib' == self.dd.model_type: + elif self.dd.model_type in ["stable_baselines3", "sb3_contrib", "pytorch"]: file_type = ".zip" + path_to_modelfile = Path(dk.data_path / f"{dk.model_filename}_model{file_type}") file_exists = path_to_modelfile.is_file() if file_exists: diff --git a/freqtrade/freqai/prediction_models/CatboostClassifier.py b/freqtrade/freqai/prediction_models/CatboostClassifier.py index ca1d8ece0..b9904e40d 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifier.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifier.py @@ -14,16 +14,20 @@ logger = logging.getLogger(__name__) class CatboostClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ train_data = Pool( diff --git a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py index c6f900fad..58c47566a 100644 --- a/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostClassifierMultiTarget.py @@ -15,16 +15,20 @@ logger = logging.getLogger(__name__) class CatboostClassifierMultiTarget(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ cbc = CatBoostClassifier( diff --git a/freqtrade/freqai/prediction_models/CatboostRegressor.py b/freqtrade/freqai/prediction_models/CatboostRegressor.py index 4b17a703b..28b1b11cc 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressor.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressor.py @@ -14,16 +14,20 @@ logger = logging.getLogger(__name__) class CatboostRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ train_data = Pool( diff --git a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py index 976d0b29b..1562c2024 100644 --- a/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/CatboostRegressorMultiTarget.py @@ -15,16 +15,20 @@ logger = logging.getLogger(__name__) class CatboostRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ cbr = CatBoostRegressor( diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifier.py b/freqtrade/freqai/prediction_models/LightGBMClassifier.py index e467ad3c1..45f3a31d0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifier.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifier.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class LightGBMClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0: diff --git a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py index d1eb6daa2..72a8ee259 100644 --- a/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMClassifierMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class LightGBMClassifierMultiTarget(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ lgb = LGBMClassifier(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressor.py b/freqtrade/freqai/prediction_models/LightGBMRegressor.py index 85c9b691c..3d1c30ed3 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressor.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressor.py @@ -12,18 +12,20 @@ logger = logging.getLogger(__name__) class LightGBMRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ - Most regressors use the same function names and arguments e.g. user - can drop in LGBMRegressor in place of CatBoostRegressor and all data - management will be properly handled by Freqai. - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ if self.freqai_info.get('data_split_parameters', {}).get('test_size', 0.1) == 0: diff --git a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py index 37c6bb186..663a611f0 100644 --- a/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/LightGBMRegressorMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class LightGBMRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ lgb = LGBMRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py new file mode 100644 index 000000000..ea7981405 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchMLPClassifier.py @@ -0,0 +1,89 @@ +from typing import Any, Dict + +import torch + +from freqtrade.freqai.base_models.BasePyTorchClassifier import BasePyTorchClassifier +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) +from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer + + +class PyTorchMLPClassifier(BasePyTorchClassifier): + """ + This class implements the fit method of IFreqaiModel. + in the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchClassifier + predict method that expects the model to predict a tensor of type long. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": null, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor( + target_tensor_type=torch.long, + squeeze_target_tensor=True + ) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + :raises ValueError: If self.class_names is not defined in the parent class. + """ + + class_names = self.get_class_names() + self.convert_label_column_to_int(data_dictionary, dk, class_names) + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=len(class_names), + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.CrossEntropyLoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + model_meta_data={"class_names": class_names}, + device=self.device, + init_model=init_model, + data_convertor=self.data_convertor, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer diff --git a/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py new file mode 100644 index 000000000..64f0f4b03 --- /dev/null +++ b/freqtrade/freqai/prediction_models/PyTorchMLPRegressor.py @@ -0,0 +1,83 @@ +from typing import Any, Dict + +import torch + +from freqtrade.freqai.base_models.BasePyTorchRegressor import BasePyTorchRegressor +from freqtrade.freqai.data_kitchen import FreqaiDataKitchen +from freqtrade.freqai.torch.PyTorchDataConvertor import (DefaultPyTorchDataConvertor, + PyTorchDataConvertor) +from freqtrade.freqai.torch.PyTorchMLPModel import PyTorchMLPModel +from freqtrade.freqai.torch.PyTorchModelTrainer import PyTorchModelTrainer + + +class PyTorchMLPRegressor(BasePyTorchRegressor): + """ + This class implements the fit method of IFreqaiModel. + in the fit method we initialize the model and trainer objects. + the only requirement from the model is to be aligned to PyTorchRegressor + predict method that expects the model to predict tensor of type float. + the trainer defines the training loop. + + parameters are passed via `model_training_parameters` under the freqai + section in the config file. e.g: + { + ... + "freqai": { + ... + "model_training_parameters" : { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 5000, + "batch_size": 64, + "max_n_eval_batches": null, + }, + "model_kwargs": { + "hidden_dim": 512, + "dropout_percent": 0.2, + "n_layer": 1, + }, + } + } + } + """ + + @property + def data_convertor(self) -> PyTorchDataConvertor: + return DefaultPyTorchDataConvertor(target_tensor_type=torch.float) + + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + config = self.freqai_info.get("model_training_parameters", {}) + self.learning_rate: float = config.get("learning_rate", 3e-4) + self.model_kwargs: Dict[str, Any] = config.get("model_kwargs", {}) + self.trainer_kwargs: Dict[str, Any] = config.get("trainer_kwargs", {}) + + def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: + """ + User sets up the training and test data to fit their desired model here + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model + """ + + n_features = data_dictionary["train_features"].shape[-1] + model = PyTorchMLPModel( + input_dim=n_features, + output_dim=1, + **self.model_kwargs + ) + model.to(self.device) + optimizer = torch.optim.AdamW(model.parameters(), lr=self.learning_rate) + criterion = torch.nn.MSELoss() + init_model = self.get_init_model(dk.pair) + trainer = PyTorchModelTrainer( + model=model, + optimizer=optimizer, + criterion=criterion, + device=self.device, + init_model=init_model, + data_convertor=self.data_convertor, + **self.trainer_kwargs, + ) + trainer.fit(data_dictionary, self.splits) + return trainer diff --git a/freqtrade/freqai/prediction_models/ReinforcementLearner.py b/freqtrade/freqai/prediction_models/ReinforcementLearner.py index e795703d4..65990da87 100644 --- a/freqtrade/freqai/prediction_models/ReinforcementLearner.py +++ b/freqtrade/freqai/prediction_models/ReinforcementLearner.py @@ -71,7 +71,8 @@ class ReinforcementLearner(BaseReinforcementLearningModel): model.learn( total_timesteps=int(total_timesteps), - callback=[self.eval_callback, self.tensorboard_callback] + callback=[self.eval_callback, self.tensorboard_callback], + progress_bar=self.rl_config.get('progress_bar', False) ) if Path(dk.data_path / "best_model.zip").is_file(): diff --git a/freqtrade/freqai/prediction_models/XGBoostClassifier.py b/freqtrade/freqai/prediction_models/XGBoostClassifier.py index 67c7c7783..b6f04b497 100644 --- a/freqtrade/freqai/prediction_models/XGBoostClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostClassifier.py @@ -18,16 +18,20 @@ logger = logging.getLogger(__name__) class XGBoostClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"].to_numpy() diff --git a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py index 470c283ea..20156e9fd 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFClassifier.py @@ -18,16 +18,20 @@ logger = logging.getLogger(__name__) class XGBoostRFClassifier(BaseClassifierModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"].to_numpy() diff --git a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py index e7cc27f2e..1aefbf19a 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRFRegressor.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class XGBoostRFRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"] diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index 9a280286b..93dfb319e 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -12,16 +12,20 @@ logger = logging.getLogger(__name__) class XGBoostRegressor(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ X = data_dictionary["train_features"] diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py index 920745ec9..a0330485e 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressorMultiTarget.py @@ -13,16 +13,20 @@ logger = logging.getLogger(__name__) class XGBoostRegressorMultiTarget(BaseRegressionModel): """ - User created prediction model. The class needs to override three necessary - functions, predict(), train(), fit(). The class inherits ModelHandler which - has its own DataHandler where data is held, saved, loaded, and managed. + User created prediction model. The class inherits IFreqaiModel, which + means it has full access to all Frequency AI functionality. Typically, + users would use this to override the common `fit()`, `train()`, or + `predict()` methods to add their custom data handling tools or change + various aspects of the training that cannot be configured via the + top level config.json file. """ def fit(self, data_dictionary: Dict, dk: FreqaiDataKitchen, **kwargs) -> Any: """ User sets up the training and test data to fit their desired model here - :param data_dictionary: the dictionary constructed by DataHandler to hold - all the training and test data/labels. + :param data_dictionary: the dictionary holding all data for train, test, + labels, weights + :param dk: The datakitchen object for the current coin/model """ xgb = XGBRegressor(**self.model_training_parameters) diff --git a/freqtrade/freqai/torch/PyTorchDataConvertor.py b/freqtrade/freqai/torch/PyTorchDataConvertor.py new file mode 100644 index 000000000..a31ccdc79 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchDataConvertor.py @@ -0,0 +1,67 @@ +from abc import ABC, abstractmethod +from typing import List, Optional + +import pandas as pd +import torch + + +class PyTorchDataConvertor(ABC): + """ + This class is responsible for converting `*_features` & `*_labels` pandas dataframes + to pytorch tensors. + """ + + @abstractmethod + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: + """ + :param df: "*_features" dataframe. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). + """ + + @abstractmethod + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: + """ + :param df: "*_labels" dataframe. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). + """ + + +class DefaultPyTorchDataConvertor(PyTorchDataConvertor): + """ + A default conversion that keeps features dataframe shapes. + """ + + def __init__( + self, + target_tensor_type: Optional[torch.dtype] = None, + squeeze_target_tensor: bool = False + ): + """ + :param target_tensor_type: type of target tensor, for classification use + torch.long, for regressor use torch.float or torch.double. + :param squeeze_target_tensor: controls the target shape, used for loss functions + that requires 0D or 1D. + """ + self._target_tensor_type = target_tensor_type + self._squeeze_target_tensor = squeeze_target_tensor + + def convert_x(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: + x = torch.from_numpy(df.values).float() + if device: + x = x.to(device) + + return [x] + + def convert_y(self, df: pd.DataFrame, device: Optional[str] = None) -> List[torch.Tensor]: + y = torch.from_numpy(df.values) + + if self._target_tensor_type: + y = y.to(self._target_tensor_type) + + if self._squeeze_target_tensor: + y = y.squeeze() + + if device: + y = y.to(device) + + return [y] diff --git a/freqtrade/freqai/torch/PyTorchMLPModel.py b/freqtrade/freqai/torch/PyTorchMLPModel.py new file mode 100644 index 000000000..62d3216df --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchMLPModel.py @@ -0,0 +1,97 @@ +import logging +from typing import List + +import torch +from torch import nn + + +logger = logging.getLogger(__name__) + + +class PyTorchMLPModel(nn.Module): + """ + A multi-layer perceptron (MLP) model implemented using PyTorch. + + This class mainly serves as a simple example for the integration of PyTorch model's + to freqai. It is not optimized at all and should not be used for production purposes. + + :param input_dim: The number of input features. This parameter specifies the number + of features in the input data that the MLP will use to make predictions. + :param output_dim: The number of output classes. This parameter specifies the number + of classes that the MLP will predict. + :param hidden_dim: The number of hidden units in each layer. This parameter controls + the complexity of the MLP and determines how many nonlinear relationships the MLP + can represent. Increasing the number of hidden units can increase the capacity of + the MLP to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 256 + :param dropout_percent: The dropout rate for regularization. This parameter specifies + the probability of dropping out a neuron during training to prevent overfitting. + The dropout rate should be tuned carefully to balance between underfitting and + overfitting. Default: 0.2 + :param n_layer: The number of layers in the MLP. This parameter specifies the number + of layers in the MLP architecture. Adding more layers to the MLP can increase its + capacity to model complex patterns, but it also increases the risk of overfitting + the training data. Default: 1 + + :returns: The output of the MLP, with shape (batch_size, output_dim) + """ + + def __init__(self, input_dim: int, output_dim: int, **kwargs): + super().__init__() + hidden_dim: int = kwargs.get("hidden_dim", 256) + dropout_percent: int = kwargs.get("dropout_percent", 0.2) + n_layer: int = kwargs.get("n_layer", 1) + self.input_layer = nn.Linear(input_dim, hidden_dim) + self.blocks = nn.Sequential(*[Block(hidden_dim, dropout_percent) for _ in range(n_layer)]) + self.output_layer = nn.Linear(hidden_dim, output_dim) + self.relu = nn.ReLU() + self.dropout = nn.Dropout(p=dropout_percent) + + def forward(self, tensors: List[torch.Tensor]) -> torch.Tensor: + x: torch.Tensor = tensors[0] + x = self.relu(self.input_layer(x)) + x = self.dropout(x) + x = self.blocks(x) + x = self.output_layer(x) + return x + + +class Block(nn.Module): + """ + A building block for a multi-layer perceptron (MLP). + + :param hidden_dim: The number of hidden units in the feedforward network. + :param dropout_percent: The dropout rate for regularization. + + :returns: torch.Tensor. with shape (batch_size, hidden_dim) + """ + + def __init__(self, hidden_dim: int, dropout_percent: int): + super().__init__() + self.ff = FeedForward(hidden_dim) + self.dropout = nn.Dropout(p=dropout_percent) + self.ln = nn.LayerNorm(hidden_dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.ff(self.ln(x)) + x = self.dropout(x) + return x + + +class FeedForward(nn.Module): + """ + A simple fully-connected feedforward neural network block. + + :param hidden_dim: The number of hidden units in the block. + :return: torch.Tensor. with shape (batch_size, hidden_dim) + """ + + def __init__(self, hidden_dim: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU(), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.net(x) diff --git a/freqtrade/freqai/torch/PyTorchModelTrainer.py b/freqtrade/freqai/torch/PyTorchModelTrainer.py new file mode 100644 index 000000000..8277ba937 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchModelTrainer.py @@ -0,0 +1,208 @@ +import logging +import math +from pathlib import Path +from typing import Any, Dict, List, Optional + +import pandas as pd +import torch +from torch import nn +from torch.optim import Optimizer +from torch.utils.data import DataLoader, TensorDataset + +from freqtrade.freqai.torch.PyTorchDataConvertor import PyTorchDataConvertor +from freqtrade.freqai.torch.PyTorchTrainerInterface import PyTorchTrainerInterface + + +logger = logging.getLogger(__name__) + + +class PyTorchModelTrainer(PyTorchTrainerInterface): + def __init__( + self, + model: nn.Module, + optimizer: Optimizer, + criterion: nn.Module, + device: str, + init_model: Dict, + data_convertor: PyTorchDataConvertor, + model_meta_data: Dict[str, Any] = {}, + **kwargs + ): + """ + :param model: The PyTorch model to be trained. + :param optimizer: The optimizer to use for training. + :param criterion: The loss function to use for training. + :param device: The device to use for training (e.g. 'cpu', 'cuda'). + :param init_model: A dictionary containing the initial model/optimizer + state_dict and model_meta_data saved by self.save() method. + :param model_meta_data: Additional metadata about the model (optional). + :param data_convertor: convertor from pd.DataFrame to torch.tensor. + :param max_iters: The number of training iterations to run. + iteration here refers to the number of times we call + self.optimizer.step(). used to calculate n_epochs. + :param batch_size: The size of the batches to use during training. + :param max_n_eval_batches: The maximum number batches to use for evaluation. + """ + self.model = model + self.optimizer = optimizer + self.criterion = criterion + self.model_meta_data = model_meta_data + self.device = device + self.max_iters: int = kwargs.get("max_iters", 100) + self.batch_size: int = kwargs.get("batch_size", 64) + self.max_n_eval_batches: Optional[int] = kwargs.get("max_n_eval_batches", None) + self.data_convertor = data_convertor + if init_model: + self.load_from_checkpoint(init_model) + + def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]): + """ + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :param splits: splits to use in training, splits must contain "train", + optional "test" could be added by setting freqai.data_split_parameters.test_size > 0 + in the config file. + + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. + """ + data_loaders_dictionary = self.create_data_loaders_dictionary(data_dictionary, splits) + epochs = self.calc_n_epochs( + n_obs=len(data_dictionary["train_features"]), + batch_size=self.batch_size, + n_iters=self.max_iters + ) + for epoch in range(1, epochs + 1): + # training + losses = [] + for i, batch_data in enumerate(data_loaders_dictionary["train"]): + + for tensor in batch_data: + tensor.to(self.device) + + xb = batch_data[:-1] + yb = batch_data[-1] + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + + self.optimizer.zero_grad(set_to_none=True) + loss.backward() + self.optimizer.step() + losses.append(loss.item()) + train_loss = sum(losses) / len(losses) + log_message = f"epoch {epoch}/{epochs}: train loss {train_loss:.4f}" + + # evaluation + if "test" in splits: + test_loss = self.estimate_loss( + data_loaders_dictionary, + self.max_n_eval_batches, + "test" + ) + log_message += f" ; test loss {test_loss:.4f}" + + logger.info(log_message) + + @torch.no_grad() + def estimate_loss( + self, + data_loader_dictionary: Dict[str, DataLoader], + max_n_eval_batches: Optional[int], + split: str, + ) -> float: + self.model.eval() + n_batches = 0 + losses = [] + for i, batch_data in enumerate(data_loader_dictionary[split]): + if max_n_eval_batches and i > max_n_eval_batches: + n_batches += 1 + break + + for tensor in batch_data: + tensor.to(self.device) + + xb = batch_data[:-1] + yb = batch_data[-1] + yb_pred = self.model(xb) + loss = self.criterion(yb_pred, yb) + losses.append(loss.item()) + + self.model.train() + return sum(losses) / len(losses) + + def create_data_loaders_dictionary( + self, + data_dictionary: Dict[str, pd.DataFrame], + splits: List[str] + ) -> Dict[str, DataLoader]: + """ + Converts the input data to PyTorch tensors using a data loader. + """ + data_loader_dictionary = {} + for split in splits: + x = self.data_convertor.convert_x(data_dictionary[f"{split}_features"], self.device) + y = self.data_convertor.convert_y(data_dictionary[f"{split}_labels"], self.device) + dataset = TensorDataset(*x, *y) + data_loader = DataLoader( + dataset, + batch_size=self.batch_size, + shuffle=True, + drop_last=True, + num_workers=0, + ) + data_loader_dictionary[split] = data_loader + + return data_loader_dictionary + + @staticmethod + def calc_n_epochs(n_obs: int, batch_size: int, n_iters: int) -> int: + """ + Calculates the number of epochs required to reach the maximum number + of iterations specified in the model training parameters. + + the motivation here is that `max_iters` is easier to optimize and keep stable, + across different n_obs - the number of data points. + """ + + n_batches = math.ceil(n_obs // batch_size) + epochs = math.ceil(n_iters // n_batches) + if epochs <= 10: + logger.warning("User set `max_iters` in such a way that the trainer will only perform " + f" {epochs} epochs. Please consider increasing this value accordingly") + if epochs <= 1: + logger.warning("Epochs set to 1. Please review your `max_iters` value") + epochs = 1 + return epochs + + def save(self, path: Path): + """ + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + """ + + torch.save({ + "model_state_dict": self.model.state_dict(), + "optimizer_state_dict": self.optimizer.state_dict(), + "model_meta_data": self.model_meta_data, + "pytrainer": self + }, path) + + def load(self, path: Path): + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + def load_from_checkpoint(self, checkpoint: Dict): + """ + when using continual_learning, DataDrawer will load the dictionary + (containing state dicts and model_meta_data) by calling torch.load(path). + you can access this dict from any class that inherits IFreqaiModel by calling + get_init_model method. + """ + self.model.load_state_dict(checkpoint["model_state_dict"]) + self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"]) + self.model_meta_data = checkpoint["model_meta_data"] + return self diff --git a/freqtrade/freqai/torch/PyTorchTrainerInterface.py b/freqtrade/freqai/torch/PyTorchTrainerInterface.py new file mode 100644 index 000000000..840c145f7 --- /dev/null +++ b/freqtrade/freqai/torch/PyTorchTrainerInterface.py @@ -0,0 +1,53 @@ +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Dict, List + +import pandas as pd +import torch +from torch import nn + + +class PyTorchTrainerInterface(ABC): + + @abstractmethod + def fit(self, data_dictionary: Dict[str, pd.DataFrame], splits: List[str]) -> None: + """ + :param data_dictionary: the dictionary constructed by DataHandler to hold + all the training and test data/labels. + :param splits: splits to use in training, splits must contain "train", + optional "test" could be added by setting freqai.data_split_parameters.test_size > 0 + in the config file. + + - Calculates the predicted output for the batch using the PyTorch model. + - Calculates the loss between the predicted and actual output using a loss function. + - Computes the gradients of the loss with respect to the model's parameters using + backpropagation. + - Updates the model's parameters using an optimizer. + """ + + @abstractmethod + def save(self, path: Path) -> None: + """ + - Saving any nn.Module state_dict + - Saving model_meta_data, this dict should contain any additional data that the + user needs to store. e.g class_names for classification models. + """ + + def load(self, path: Path) -> nn.Module: + """ + :param path: path to zip file. + :returns: pytorch model. + """ + checkpoint = torch.load(path) + return self.load_from_checkpoint(checkpoint) + + @abstractmethod + def load_from_checkpoint(self, checkpoint: Dict) -> nn.Module: + """ + when using continual_learning, DataDrawer will load the dictionary + (containing state dicts and model_meta_data) by calling torch.load(path). + you can access this dict from any class that inherits IFreqaiModel by calling + get_init_model method. + :checkpoint checkpoint: dict containing the model & optimizer state dicts, + model_meta_data, etc.. + """ diff --git a/freqtrade/freqai/torch/__init__.py b/freqtrade/freqai/torch/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9746ac3d8..9cc26ad77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -21,10 +21,12 @@ from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, State, TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds +from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, timeframe_to_next_date, + timeframe_to_seconds) from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, init_db +from freqtrade.persistence.key_value_store import set_startup_time from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -181,6 +183,7 @@ class FreqtradeBot(LoggingMixin): performs startup tasks """ migrate_binance_futures_names(self.config) + set_startup_time() self.rpc.startup_messages(self.config, self.pairlists, self.protections) # Update older trades with precision and precision mode @@ -853,7 +856,8 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Canceling stoploss on exchange for {trade}") co = self.exchange.cancel_stoploss_order_with_result( trade.stoploss_order_id, trade.pair, trade.amount) - trade.update_order(co) + self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True) + # Reset stoploss order id. trade.stoploss_order_id = None except InvalidOrderException: @@ -945,7 +949,7 @@ class FreqtradeBot(LoggingMixin): return enter_limit_requested, stake_amount, leverage - def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None, + def _notify_enter(self, trade: Trade, order: Order, order_type: str, fill: bool = False, sub_trade: bool = False) -> None: """ Sends rpc notification when a entry order occurred. @@ -1171,7 +1175,8 @@ class FreqtradeBot(LoggingMixin): logger.warning('Unable to fetch stoploss order: %s', exception) if stoploss_order: - trade.update_order(stoploss_order) + self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, + stoploss_order=True) # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): @@ -1235,7 +1240,9 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation) + stoploss_norm = self.exchange.price_to_precision( + trade.pair, trade.stoploss_or_liquidation, + rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP) if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side): # we check if the update is necessary @@ -1418,7 +1425,7 @@ class FreqtradeBot(LoggingMixin): corder = order reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('%s order %s for %s.', side, reason, trade) + logger.info(f'{side} order {reason} for {trade}.') # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') @@ -1478,8 +1485,8 @@ class FreqtradeBot(LoggingMixin): return False try: - order = self.exchange.cancel_order_with_result(order['id'], trade.pair, - trade.amount) + order = self.exchange.cancel_order_with_result( + order['id'], trade.pair, trade.amount) except InvalidOrderException: logger.exception( f"Could not cancel {trade.exit_side} order {trade.open_order_id}") @@ -1491,17 +1498,18 @@ class FreqtradeBot(LoggingMixin): # Order might be filled above in odd timing issues. if order.get('status') in ('canceled', 'cancelled'): trade.exit_reason = None + trade.open_order_id = None else: trade.exit_reason = exit_reason_prev cancelled = True else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] trade.exit_reason = None + trade.open_order_id = None - self.update_trade_state(trade, trade.open_order_id, order) + self.update_trade_state(trade, order['id'], order) logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.') - trade.open_order_id = None trade.close_rate = None trade.close_rate_requested = None @@ -1778,11 +1786,11 @@ class FreqtradeBot(LoggingMixin): return False # Update trade with order values - logger.info(f'Found open order for {trade}') + if not stoploss_order: + logger.info(f'Found open order for {trade}') try: - order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, - trade.pair, - stoploss_order) + order = action_order or self.exchange.fetch_order_or_stoploss_order( + order_id, trade.pair, stoploss_order) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False @@ -1847,7 +1855,7 @@ class FreqtradeBot(LoggingMixin): self.handle_protections(trade.pair, trade.trade_direction) elif send_msg and not trade.open_order_id and not stoploss_order: # Enter fill - self._notify_enter(trade, order, fill=True, sub_trade=sub_trade) + self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade) def handle_protections(self, pair: str, side: LongShort) -> None: # Lock pair for one candle to prevent immediate rebuys diff --git a/freqtrade/loggers.py b/freqtrade/loggers/__init__.py similarity index 88% rename from freqtrade/loggers.py rename to freqtrade/loggers/__init__.py index 823fa174e..528d274f2 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers/__init__.py @@ -1,24 +1,11 @@ import logging -import sys from logging import Formatter -from logging.handlers import BufferingHandler, RotatingFileHandler, SysLogHandler +from logging.handlers import RotatingFileHandler, SysLogHandler from freqtrade.constants import Config from freqtrade.exceptions import OperationalException - - -class FTBufferingHandler(BufferingHandler): - def flush(self): - """ - Override Flush behaviour - we keep half of the configured capacity - otherwise, we have moments with "empty" logs. - """ - self.acquire() - try: - # Keep half of the records in buffer. - self.buffer = self.buffer[-int(self.capacity / 2):] - finally: - self.release() +from freqtrade.loggers.buffering_handler import FTBufferingHandler +from freqtrade.loggers.std_err_stream_handler import FTStdErrStreamHandler logger = logging.getLogger(__name__) @@ -69,7 +56,7 @@ def setup_logging_pre() -> None: logging.basicConfig( level=logging.INFO, format=LOGFORMAT, - handlers=[logging.StreamHandler(sys.stderr), bufferHandler] + handlers=[FTStdErrStreamHandler(), bufferHandler] ) diff --git a/freqtrade/loggers/buffering_handler.py b/freqtrade/loggers/buffering_handler.py new file mode 100644 index 000000000..e4621fa79 --- /dev/null +++ b/freqtrade/loggers/buffering_handler.py @@ -0,0 +1,15 @@ +from logging.handlers import BufferingHandler + + +class FTBufferingHandler(BufferingHandler): + def flush(self): + """ + Override Flush behaviour - we keep half of the configured capacity + otherwise, we have moments with "empty" logs. + """ + self.acquire() + try: + # Keep half of the records in buffer. + self.buffer = self.buffer[-int(self.capacity / 2):] + finally: + self.release() diff --git a/freqtrade/loggers/std_err_stream_handler.py b/freqtrade/loggers/std_err_stream_handler.py new file mode 100644 index 000000000..487a7c100 --- /dev/null +++ b/freqtrade/loggers/std_err_stream_handler.py @@ -0,0 +1,26 @@ +import sys +from logging import Handler + + +class FTStdErrStreamHandler(Handler): + def flush(self): + """ + Override Flush behaviour - we keep half of the configured capacity + otherwise, we have moments with "empty" logs. + """ + self.acquire() + try: + sys.stderr.flush() + finally: + self.release() + + def emit(self, record): + try: + msg = self.format(record) + # Don't keep a reference to stderr - this can be problematic with progressbars. + sys.stderr.write(msg + '\n') + self.flush() + except RecursionError: + raise + except Exception: + self.handleError(record) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 96c95c4a2..fe590f0d2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -13,13 +13,13 @@ from math import ceil from pathlib import Path from typing import Any, Dict, List, Optional, Tuple -import progressbar import rapidjson -from colorama import Fore, Style from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from joblib.externals import cloudpickle from pandas import DataFrame +from rich.progress import (BarColumn, MofNCompleteColumn, Progress, TaskProgressColumn, TextColumn, + TimeElapsedColumn, TimeRemainingColumn) from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN, Config from freqtrade.data.converter import trim_dataframes @@ -44,8 +44,6 @@ with warnings.catch_warnings(): from skopt import Optimizer from skopt.space import Dimension -progressbar.streams.wrap_stderr() -progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) @@ -381,7 +379,8 @@ class Hyperopt: strat_stats = generate_strategy_stats( self.pairlist, self.backtesting.strategy.get_strategy_name(), - backtesting_results, min_date, max_date, market_change=self.market_change + backtesting_results, min_date, max_date, market_change=self.market_change, + is_hyperopt=True, ) results_explanation = HyperoptTools.format_results_explanation_string( strat_stats, self.config['stake_currency']) @@ -520,29 +519,6 @@ class Hyperopt: else: return self.opt.ask(n_points=n_points), [False for _ in range(n_points)] - def get_progressbar_widgets(self): - if self.print_colorized: - widgets = [ - ' [Epoch ', progressbar.Counter(), ' of ', str(self.total_epochs), - ' (', progressbar.Percentage(), ')] ', - progressbar.Bar(marker=progressbar.AnimatedMarker( - fill='\N{FULL BLOCK}', - fill_wrap=Fore.GREEN + '{}' + Fore.RESET, - marker_wrap=Style.BRIGHT + '{}' + Style.RESET_ALL, - )), - ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', - ] - else: - widgets = [ - ' [Epoch ', progressbar.Counter(), ' of ', str(self.total_epochs), - ' (', progressbar.Percentage(), ')] ', - progressbar.Bar(marker=progressbar.AnimatedMarker( - fill='\N{FULL BLOCK}', - )), - ' [', progressbar.ETA(), ', ', progressbar.Timer(), ']', - ] - return widgets - def evaluate_result(self, val: Dict[str, Any], current: int, is_random: bool): """ Evaluate results returned from generate_optimizer @@ -602,11 +578,19 @@ class Hyperopt: logger.info(f'Effective number of parallel workers used: {jobs}') # Define progressbar - widgets = self.get_progressbar_widgets() - with progressbar.ProgressBar( - max_value=self.total_epochs, redirect_stdout=False, redirect_stderr=False, - widgets=widgets + with Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(bar_width=None), + MofNCompleteColumn(), + TaskProgressColumn(), + "•", + TimeElapsedColumn(), + "•", + TimeRemainingColumn(), + expand=True, ) as pbar: + task = pbar.add_task("Epochs", total=self.total_epochs) + start = 0 if self.analyze_per_epoch: @@ -616,7 +600,7 @@ class Hyperopt: f_val0 = self.generate_optimizer(asked[0]) self.opt.tell(asked, [f_val0['loss']]) self.evaluate_result(f_val0, 1, is_random[0]) - pbar.update(1) + pbar.update(task, advance=1) start += 1 evals = ceil((self.total_epochs - start) / jobs) @@ -630,14 +614,12 @@ class Hyperopt: f_val = self.run_optimizer_parallel(parallel, asked) self.opt.tell(asked, [v['loss'] for v in f_val]) - # Calculate progressbar outputs for j, val in enumerate(f_val): # Use human-friendly indexes here (starting from 1) current = i * jobs + j + 1 + start self.evaluate_result(val, current, is_random[j]) - - pbar.update(current) + pbar.update(task, advance=1) except KeyboardInterrupt: print('User interrupted..') diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index e2133a956..1e7befdf6 100644 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -23,6 +23,8 @@ logger = logging.getLogger(__name__) NON_OPT_PARAM_APPENDIX = " # value loaded from strategy" +HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN + def hyperopt_serializer(x): if isinstance(x, np.integer): @@ -76,9 +78,18 @@ class HyperoptTools(): with filename.open('w') as f: rapidjson.dump(final_params, f, indent=2, default=hyperopt_serializer, - number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN + number_mode=HYPER_PARAMS_FILE_FORMAT ) + @staticmethod + def load_params(filename: Path) -> Dict: + """ + Load parameters from file + """ + with filename.open('r') as f: + params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT) + return params + @staticmethod def try_export_params(config: Config, strategy_name: str, params: Dict): if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False): @@ -189,7 +200,7 @@ class HyperoptTools(): for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing', 'max_open_trades']: HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s) - print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) + print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT)) else: HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:", diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 83f698fbe..1c5088cc1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -7,8 +7,8 @@ from typing import Any, Dict, List, Union from pandas import DataFrame, to_datetime from tabulate import tabulate -from freqtrade.constants import (DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT, - Config, IntOrInf) +from freqtrade.constants import (BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, + UNLIMITED_STAKE_AMOUNT, Config, IntOrInf) from freqtrade.data.metrics import (calculate_cagr, calculate_calmar, calculate_csum, calculate_expectancy, calculate_market_change, calculate_max_drawdown, calculate_sharpe, calculate_sortino) @@ -273,7 +273,8 @@ def _get_resample_from_period(period: str) -> str: if period == 'day': return '1d' if period == 'week': - return '1w' + # Weekly defaulting to Monday. + return '1W-MON' if period == 'month': return '1M' raise ValueError(f"Period {period} is not supported.") @@ -295,6 +296,7 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic stats.append( { 'date': name.strftime('%d/%m/%Y'), + 'date_ts': int(name.to_pydatetime().timestamp() * 1000), 'profit_abs': profit_abs, 'wins': wins, 'draws': draws, @@ -304,6 +306,13 @@ def generate_periodic_breakdown_stats(trade_list: List, period: str) -> List[Dic return stats +def generate_all_periodic_breakdown_stats(trade_list: List) -> Dict[str, List]: + result = {} + for period in BACKTEST_BREAKDOWNS: + result[period] = generate_periodic_breakdown_stats(trade_list, period) + return result + + def generate_trading_stats(results: DataFrame) -> Dict[str, Any]: """ Generate overall trade statistics """ if len(results) == 0: @@ -380,7 +389,8 @@ def generate_strategy_stats(pairlist: List[str], strategy: str, content: Dict[str, Any], min_date: datetime, max_date: datetime, - market_change: float + market_change: float, + is_hyperopt: bool = False, ) -> Dict[str, Any]: """ :param pairlist: List of pairs to backtest @@ -415,6 +425,11 @@ def generate_strategy_stats(pairlist: List[str], daily_stats = generate_daily_stats(results) trade_stats = generate_trading_stats(results) + + periodic_breakdown = {} + if not is_hyperopt: + periodic_breakdown = {'periodic_breakdown': generate_all_periodic_breakdown_stats(results)} + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], @@ -433,7 +448,6 @@ def generate_strategy_stats(pairlist: List[str], 'results_per_enter_tag': enter_tag_results, 'exit_reason_summary': exit_reason_stats, 'left_open_trades': left_open_results, - # 'days_breakdown_stats': days_breakdown_stats, 'total_trades': len(results), 'trade_count_long': len(results.loc[~results['is_short']]), @@ -498,6 +512,7 @@ def generate_strategy_stats(pairlist: List[str], 'exit_profit_only': config['exit_profit_only'], 'exit_profit_offset': config['exit_profit_offset'], 'ignore_roi_if_entry_signal': config['ignore_roi_if_entry_signal'], + **periodic_breakdown, **daily_stats, **trade_stats } @@ -865,6 +880,11 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(table) + table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) + if isinstance(table, str) and len(table) > 0: + print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) + print(table) + if (results.get('results_per_enter_tag') is not None or results.get('results_per_buy_tag') is not None): # results_per_buy_tag is deprecated and should be removed 2 versions after short golive. @@ -884,14 +904,12 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency: print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) - table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) - if isinstance(table, str) and len(table) > 0: - print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) - print(table) - for period in backtest_breakdown: - days_breakdown_stats = generate_periodic_breakdown_stats( - trade_list=results['trades'], period=period) + if period in results.get('periodic_breakdown', {}): + days_breakdown_stats = results['periodic_breakdown'][period] + else: + days_breakdown_stats = generate_periodic_breakdown_stats( + trade_list=results['trades'], period=period) table = text_table_periodic_breakdown(days_breakdown_stats=days_breakdown_stats, stake_currency=stake_currency, period=period) if isinstance(table, str) and len(table) > 0: @@ -917,11 +935,11 @@ def show_backtest_results(config: Config, backtest_stats: Dict): strategy, results, stake_currency, config.get('backtest_breakdown', [])) - if len(backtest_stats['strategy']) > 1: + if len(backtest_stats['strategy']) > 0: # Print Strategy summary table table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency) - print(f"{results['backtest_start']} -> {results['backtest_end']} |" + print(f"Backtested {results['backtest_start']} -> {results['backtest_end']} |" f" Max open trades : {results['max_open_trades']}") print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '=')) print(table) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 9e1a7e922..4cf7aa455 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py new file mode 100644 index 000000000..2d26acbd3 --- /dev/null +++ b/freqtrade/persistence/key_value_store.py @@ -0,0 +1,179 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import ClassVar, Optional, Union + +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from freqtrade.persistence.base import ModelBase, SessionType + + +ValueTypes = Union[str, datetime, float, int] + + +class ValueTypesEnum(str, Enum): + STRING = 'str' + DATETIME = 'datetime' + FLOAT = 'float' + INT = 'int' + + +class KeyStoreKeys(str, Enum): + BOT_START_TIME = 'bot_start_time' + STARTUP_TIME = 'startup_time' + + +class _KeyValueStoreModel(ModelBase): + """ + Pair Locks database model. + """ + __tablename__ = 'KeyValueStore' + session: ClassVar[SessionType] + + id: Mapped[int] = mapped_column(primary_key=True) + + key: Mapped[KeyStoreKeys] = mapped_column(String(25), nullable=False, index=True) + + value_type: Mapped[ValueTypesEnum] = mapped_column(String(20), nullable=False) + + string_value: Mapped[Optional[str]] + datetime_value: Mapped[Optional[datetime]] + float_value: Mapped[Optional[float]] + int_value: Mapped[Optional[int]] + + +class KeyValueStore(): + """ + Generic bot-wide, persistent key-value store + Can be used to store generic values, e.g. very first bot startup time. + Supports the types str, datetime, float and int. + """ + + @staticmethod + def store_value(key: KeyStoreKeys, value: ValueTypes) -> None: + """ + Store the given value for the given key. + :param key: Key to store the value for - can be used in get-value to retrieve the key + :param value: Value to store - can be str, datetime, float or int + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + kv = _KeyValueStoreModel(key=key) + if isinstance(value, str): + kv.value_type = ValueTypesEnum.STRING + kv.string_value = value + elif isinstance(value, datetime): + kv.value_type = ValueTypesEnum.DATETIME + kv.datetime_value = value + elif isinstance(value, float): + kv.value_type = ValueTypesEnum.FLOAT + kv.float_value = value + elif isinstance(value, int): + kv.value_type = ValueTypesEnum.INT + kv.int_value = value + else: + raise ValueError(f'Unknown value type {kv.value_type}') + _KeyValueStoreModel.session.add(kv) + _KeyValueStoreModel.session.commit() + + @staticmethod + def delete_value(key: KeyStoreKeys) -> None: + """ + Delete the value for the given key. + :param key: Key to delete the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is not None: + _KeyValueStoreModel.session.delete(kv) + _KeyValueStoreModel.session.commit() + + @staticmethod + def get_value(key: KeyStoreKeys) -> Optional[ValueTypes]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key).first() + if kv is None: + return None + if kv.value_type == ValueTypesEnum.STRING: + return kv.string_value + if kv.value_type == ValueTypesEnum.DATETIME and kv.datetime_value is not None: + return kv.datetime_value.replace(tzinfo=timezone.utc) + if kv.value_type == ValueTypesEnum.FLOAT: + return kv.float_value + if kv.value_type == ValueTypesEnum.INT: + return kv.int_value + # This should never happen unless someone messed with the database manually + raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover + + @staticmethod + def get_string_value(key: KeyStoreKeys) -> Optional[str]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.STRING).first() + if kv is None: + return None + return kv.string_value + + @staticmethod + def get_datetime_value(key: KeyStoreKeys) -> Optional[datetime]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME).first() + if kv is None or kv.datetime_value is None: + return None + return kv.datetime_value.replace(tzinfo=timezone.utc) + + @staticmethod + def get_float_value(key: KeyStoreKeys) -> Optional[float]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT).first() + if kv is None: + return None + return kv.float_value + + @staticmethod + def get_int_value(key: KeyStoreKeys) -> Optional[int]: + """ + Get the value for the given key. + :param key: Key to get the value for + """ + kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.INT).first() + if kv is None: + return None + return kv.int_value + + +def set_startup_time(): + """ + sets bot_start_time to the first trade open date - or "now" on new databases. + sets startup_time to "now" + """ + st = KeyValueStore.get_value('bot_start_time') + if st is None: + from freqtrade.persistence import Trade + t = Trade.session.query(Trade).order_by(Trade.open_date.asc()).first() + if t is not None: + KeyValueStore.store_value('bot_start_time', t.open_date_utc) + else: + KeyValueStore.store_value('bot_start_time', datetime.now(timezone.utc)) + KeyValueStore.store_value('startup_time', datetime.now(timezone.utc)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2315c0acc..e561e727b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -13,6 +13,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.persistence.base import ModelBase +from freqtrade.persistence.key_value_store import _KeyValueStoreModel from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade @@ -76,6 +77,7 @@ def init_db(db_url: str) -> None: bind=engine, autoflush=False), scopefunc=get_request_or_thread_id) Order.session = Trade.session PairLock.session = Trade.session + _KeyValueStoreModel.session = Trade.session previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 17117d436..0572b45a6 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -9,13 +9,14 @@ from typing import Any, ClassVar, Dict, List, Optional, Sequence, cast from sqlalchemy import (Enum, Float, ForeignKey, Integer, ScalarResult, Select, String, UniqueConstraint, desc, func, select) -from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship +from sqlalchemy.orm import Mapped, lazyload, mapped_column, relationship, validates -from freqtrade.constants import (DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, NON_OPEN_EXCHANGE_STATES, - BuySell, LongShort) +from freqtrade.constants import (CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT, MATH_CLOSE_PREC, + NON_OPEN_EXCHANGE_STATES, BuySell, LongShort) from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.exchange import amount_to_contract_precision, price_to_precision +from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precision, + price_to_precision) from freqtrade.leverage import interest from freqtrade.persistence.base import ModelBase, SessionType from freqtrade.util import FtPrecise @@ -597,7 +598,8 @@ class LocalTrade(): """ Method used internally to set self.stop_loss. """ - stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode) + stop_loss_norm = price_to_precision(stop_loss, self.price_precision, self.precision_mode, + rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) if not self.stop_loss: self.initial_stop_loss = stop_loss_norm self.stop_loss = stop_loss_norm @@ -628,7 +630,8 @@ class LocalTrade(): if self.initial_stop_loss_pct is None or refresh: self.__set_stop_loss(new_loss, stoploss) self.initial_stop_loss = price_to_precision( - new_loss, self.price_precision, self.precision_mode) + new_loss, self.price_precision, self.precision_mode, + rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) self.initial_stop_loss_pct = -1 * abs(stoploss) # evaluate if the stop loss needs to be updated @@ -692,21 +695,24 @@ class LocalTrade(): else: logger.warning( f'Got different open_order_id {self.open_order_id} != {order.order_id}') + + elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): + self.stoploss_order_id = None + self.close_rate_requested = self.stop_loss + self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value + if self.is_open: + logger.info(f'{order.order_type.upper()} is hit for {self}.') + else: + raise ValueError(f'Unknown order type: {order.order_type}') + + if order.ft_order_side != self.entry_side: amount_tr = amount_to_contract_precision(self.amount, self.amount_precision, self.precision_mode, self.contract_size) if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC): self.close(order.safe_price) else: self.recalc_trade_from_orders() - elif order.ft_order_side == 'stoploss' and order.status not in ('canceled', 'open'): - self.stoploss_order_id = None - self.close_rate_requested = self.stop_loss - self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value - if self.is_open: - logger.info(f'{order.order_type.upper()} is hit for {self}.') - self.close(order.safe_price) - else: - raise ValueError(f'Unknown order type: {order.order_type}') + Trade.commit() def close(self, rate: float, *, show_msg: bool = True) -> None: @@ -1253,11 +1259,13 @@ class Trade(ModelBase, LocalTrade): Float(), nullable=True, default=0.0) # type: ignore # Lowest price reached min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore - exit_reason: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore + exit_reason: Mapped[Optional[str]] = mapped_column( + String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore exit_order_status: Mapped[Optional[str]] = mapped_column( String(100), nullable=True) # type: ignore strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore - enter_tag: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore + enter_tag: Mapped[Optional[str]] = mapped_column( + String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore trading_mode: Mapped[TradingMode] = mapped_column( @@ -1287,6 +1295,13 @@ class Trade(ModelBase, LocalTrade): self.realized_profit = 0 self.recalc_open_trade_value() + @validates('enter_tag', 'exit_reason') + def validate_string_len(self, key, value): + max_len = getattr(self.__class__, key).prop.columns[0].type.length + if value and len(value) > max_len: + return value[:max_len] + return value + def delete(self) -> None: for order in self.orders: diff --git a/freqtrade/plugins/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py index 478eaec20..2e74aa293 100644 --- a/freqtrade/plugins/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -6,6 +6,7 @@ from typing import Any, Dict, Optional from freqtrade.constants import Config from freqtrade.exceptions import OperationalException +from freqtrade.exchange import ROUND_UP from freqtrade.exchange.types import Ticker from freqtrade.plugins.pairlist.IPairList import IPairList @@ -61,9 +62,10 @@ class PrecisionFilter(IPairList): stop_price = ticker['last'] * self._stoploss # Adjust stop-prices to precision - sp = self._exchange.price_to_precision(pair, stop_price) + sp = self._exchange.price_to_precision(pair, stop_price, rounding_mode=ROUND_UP) - stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99) + stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99, + rounding_mode=ROUND_UP) logger.debug(f"{pair} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index 764c16f1a..d077330e0 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -143,6 +143,9 @@ class RemotePairList(IPairList): if self._init_done: pairlist = self._pair_cache.get('pairlist') + if pairlist == [None]: + # Valid but empty pairlist. + return [] else: pairlist = [] @@ -181,7 +184,11 @@ class RemotePairList(IPairList): pairlist = self._whitelist_for_active_markets(pairlist) pairlist = pairlist[:self._number_pairs] - self._pair_cache['pairlist'] = pairlist.copy() + if pairlist: + self._pair_cache['pairlist'] = pairlist.copy() + else: + # If pairlist is empty, set a dummy value to avoid fetching again + self._pair_cache['pairlist'] = [None] if time_elapsed != 0.0: self.log_once(f'Pairlist Fetched in {time_elapsed} seconds.', logger.info) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 7497b27f1..53bf7558f 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -108,6 +108,8 @@ class Profit(BaseModel): max_drawdown: float max_drawdown_abs: float trading_volume: Optional[float] + bot_start_timestamp: int + bot_start_date: str class SellReason(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 8ea70bb69..8aa706e62 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -303,11 +303,11 @@ def get_strategy(strategy: str, config=Depends(get_config)): @router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai']) def list_freqaimodels(config=Depends(get_config)): from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver - strategies = FreqaiModelResolver.search_all_objects( + models = FreqaiModelResolver.search_all_objects( config, False) - strategies = sorted(strategies, key=lambda x: x['name']) + models = sorted(models, key=lambda x: x['name']) - return {'freqaimodels': [x['name'] for x in strategies]} + return {'freqaimodels': [x['name'] for x in models]} @router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data']) diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index a79c1a5fc..48786bec2 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -55,7 +55,7 @@ class UvicornServer(uvicorn.Server): @contextlib.contextmanager def run_in_thread(self): - self.thread = threading.Thread(target=self.run) + self.thread = threading.Thread(target=self.run, name='FTUvicorn') self.thread.start() while not self.started: time.sleep(1e-3) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2b5eb107c..814f0d6a8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -24,9 +24,10 @@ from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirecti State, TradingMode) from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs +from freqtrade.exchange.types import Tickers from freqtrade.loggers import bufferHandler from freqtrade.misc import decimals_per_coin, shorten_date -from freqtrade.persistence import Order, PairLocks, Trade +from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -543,6 +544,7 @@ class RPC: first_date = trades[0].open_date if trades else None last_date = trades[-1].open_date if trades else None num = float(len(durations) or 1) + bot_start = KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME) return { 'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2), @@ -576,17 +578,44 @@ class RPC: 'max_drawdown': max_drawdown, 'max_drawdown_abs': max_drawdown_abs, 'trading_volume': trading_volume, + 'bot_start_timestamp': int(bot_start.timestamp() * 1000) if bot_start else 0, + 'bot_start_date': bot_start.strftime(DATETIME_PRINT_FORMAT) if bot_start else '', } + def __balance_get_est_stake( + self, coin: str, stake_currency: str, balance: Wallet, tickers) -> float: + est_stake = 0.0 + if coin == stake_currency: + est_stake = balance.total + if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: + # in Futures, "total" includes the locked stake, and therefore all positions + est_stake = balance.free + else: + try: + pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) + rate: Optional[float] = tickers.get(pair, {}).get('last', None) + if rate: + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): + rate = 1.0 / rate + est_stake = rate * balance.total + except (ExchangeError): + logger.warning(f"Could not get rate for pair {coin}.") + raise ValueError() + + return est_stake + def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ currencies: List[Dict] = [] total = 0.0 + total_bot = 0.0 try: - tickers = self._freqtrade.exchange.get_tickers(cached=True) + tickers: Tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') + open_trades: List[Trade] = Trade.get_open_trades() + open_assets = [t.base_currency for t in open_trades] self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() starting_cap_fiat = self._fiat_converter.convert_amount( @@ -596,26 +625,14 @@ class RPC: for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: continue + try: + est_stake = self.__balance_get_est_stake(coin, stake_currency, balance, tickers) + except ValueError: + continue - est_stake: float = 0 - if coin == stake_currency: - rate = 1.0 - est_stake = balance.total - if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - # in Futures, "total" includes the locked stake, and therefore all positions - est_stake = balance.free - else: - try: - pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) - rate = tickers.get(pair, {}).get('last') - if rate: - if pair.startswith(stake_currency) and not pair.endswith(stake_currency): - rate = 1.0 / rate - est_stake = rate * balance.total - except (ExchangeError): - logger.warning(f" Could not get rate for pair {coin}.") - continue - total = total + est_stake + total += est_stake + if coin == stake_currency or coin in open_assets: + total_bot += est_stake currencies.append({ 'currency': coin, 'free': balance.free, @@ -648,10 +665,12 @@ class RPC: value = self._fiat_converter.convert_amount( total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + value_bot = self._fiat_converter.convert_amount( + total_bot, stake_currency, fiat_display_currency) if self._fiat_converter else 0 trade_count = len(Trade.get_trades_proxy()) - starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 - starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 + starting_capital_ratio = (total_bot / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value_bot / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { 'currencies': currencies, @@ -1193,6 +1212,7 @@ class RPC: from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(config) strategy.dp = DataProvider(config, exchange=exchange, pairlists=None) + strategy.ft_bot_start() df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) diff --git a/freqtrade/rpc/rpc_types.py b/freqtrade/rpc/rpc_types.py index 3277a2d6e..23f3ed5a9 100644 --- a/freqtrade/rpc/rpc_types.py +++ b/freqtrade/rpc/rpc_types.py @@ -52,7 +52,7 @@ class __RPCBuyMsgBase(RPCSendMsgBase): direction: str limit: float open_rate: float - order_type: Optional[str] # TODO: why optional?? + order_type: str stake_amount: float stake_currency: str fiat_currency: Optional[str] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d79d8ea76..e626ee598 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -66,10 +66,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: chat_id = int(self._config['telegram']['chat_id']) if cchat_id != chat_id: - logger.info( - 'Rejected unauthorized message from: %s', - update.message.chat_id - ) + logger.info(f'Rejected unauthorized message from: {update.message.chat_id}') return wrapper # Rollback session to avoid getting data stored in a transaction. Trade.rollback() @@ -819,7 +816,7 @@ class Telegram(RPCHandler): best_pair = stats['best_pair'] best_pair_profit_ratio = stats['best_pair_profit_ratio'] if stats['trade_count'] == 0: - markdown_msg = 'No trades yet.' + markdown_msg = f"No trades yet.\n*Bot started:* `{stats['bot_start_date']}`" else: # Message to display if stats['closed_trade_count'] > 0: @@ -838,6 +835,7 @@ class Telegram(RPCHandler): f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" f"*Total Trade Count:* `{trade_count}`\n" + f"*Bot started:* `{stats['bot_start_date']}`\n" f"*{'First Trade opened' if not timescale else 'Showing Profit since'}:* " f"`{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}`\n" @@ -1420,7 +1418,7 @@ class Telegram(RPCHandler): def send_blacklist_msg(self, blacklist: Dict): errmsgs = [] for pair, error in blacklist['errors'].items(): - errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`") + errmsgs.append(f"Error: {error['error_msg']}") if errmsgs: self._send_msg('\n'.join(errmsgs)) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 52ba22951..d38110a2a 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -8,7 +8,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union from freqtrade.constants import Config from freqtrade.exceptions import OperationalException -from freqtrade.misc import deep_merge_dicts, json_load +from freqtrade.misc import deep_merge_dicts from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.strategy.parameters import BaseParameter @@ -124,8 +124,7 @@ class HyperStrategyMixin: if filename.is_file(): logger.info(f"Loading parameters from file {filename}") try: - with filename.open('r') as f: - params = json_load(f) + params = HyperoptTools.load_params(filename) if params.get('strategy_name') != self.__class__.__name__: raise OperationalException('Invalid parameter file provided.') return params diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6d4a3036f..7adb7a154 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -10,7 +10,7 @@ from typing import Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame -from freqtrade.constants import Config, IntOrInf, ListPairsWithTimeframes +from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, Config, IntOrInf, ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, MarketDirection, RunMode, SignalDirection, SignalTagType, SignalType, TradingMode) @@ -27,7 +27,6 @@ from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) -CUSTOM_EXIT_MAX_LENGTH = 64 class IStrategy(ABC, HyperStrategyMixin): @@ -619,7 +618,7 @@ class IStrategy(ABC, HyperStrategyMixin): return df def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -645,7 +644,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -674,7 +674,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -698,7 +699,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -1118,11 +1119,11 @@ class IStrategy(ABC, HyperStrategyMixin): exit_signal = ExitType.CUSTOM_EXIT if isinstance(reason_cust, str): custom_reason = reason_cust - if len(reason_cust) > CUSTOM_EXIT_MAX_LENGTH: + if len(reason_cust) > CUSTOM_TAG_MAX_LENGTH: logger.warning(f'Custom exit reason returned from ' f'custom_exit is too long and was trimmed' - f'to {CUSTOM_EXIT_MAX_LENGTH} characters.') - custom_reason = reason_cust[:CUSTOM_EXIT_MAX_LENGTH] + f'to {CUSTOM_TAG_MAX_LENGTH} characters.') + custom_reason = reason_cust[:CUSTOM_TAG_MAX_LENGTH] else: custom_reason = '' if ( diff --git a/freqtrade/templates/FreqaiExampleHybridStrategy.py b/freqtrade/templates/FreqaiExampleHybridStrategy.py index 0e7113f8c..03446d76e 100644 --- a/freqtrade/templates/FreqaiExampleHybridStrategy.py +++ b/freqtrade/templates/FreqaiExampleHybridStrategy.py @@ -97,7 +97,7 @@ class FreqaiExampleHybridStrategy(IStrategy): exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -151,7 +151,8 @@ class FreqaiExampleHybridStrategy(IStrategy): return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -183,7 +184,8 @@ class FreqaiExampleHybridStrategy(IStrategy): dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -209,7 +211,7 @@ class FreqaiExampleHybridStrategy(IStrategy): dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. @@ -223,6 +225,7 @@ class FreqaiExampleHybridStrategy(IStrategy): :param metadata: metadata of current pair usage example: dataframe["&-target"] = dataframe["close"].shift(-1) / dataframe["close"] """ + self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-50) > dataframe["close"], 'up', 'down') diff --git a/freqtrade/templates/FreqaiExampleStrategy.py b/freqtrade/templates/FreqaiExampleStrategy.py index 0093c7f7a..493ea17f3 100644 --- a/freqtrade/templates/FreqaiExampleStrategy.py +++ b/freqtrade/templates/FreqaiExampleStrategy.py @@ -48,7 +48,7 @@ class FreqaiExampleStrategy(IStrategy): [0.75, 1, 1.25, 1.5, 1.75], space="sell", default=1.25, optimize=True) def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, - metadata: Dict, **kwargs): + metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -106,7 +106,8 @@ class FreqaiExampleStrategy(IStrategy): return dataframe - def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_expand_basic( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This function will automatically expand the defined features on the config defined @@ -142,7 +143,8 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-raw_price"] = dataframe["close"] return dataframe - def feature_engineering_standard(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def feature_engineering_standard( + self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* This optional function will be called once with the dataframe of the base timeframe. @@ -172,7 +174,7 @@ class FreqaiExampleStrategy(IStrategy): dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe - def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): + def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs) -> DataFrame: """ *Only functional with FreqAI enabled strategies* Required function to set the targets for the model. diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 8dcc92af4..6f86398f3 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -11,6 +11,7 @@ from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, Config from freqtrade.enums import RunMode, TradingMode from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange +from freqtrade.misc import safe_value_fallback from freqtrade.persistence import LocalTrade, Trade @@ -148,7 +149,7 @@ class Wallets: # Position is not open ... continue size = self._exchange._contracts_to_amount(symbol, position['contracts']) - collateral = position['collateral'] or 0.0 + collateral = safe_value_fallback(position, 'collateral', 'initialMargin', 0.0) leverage = position['leverage'] self._positions[symbol] = PositionWallet( symbol, position=size, diff --git a/pyproject.toml b/pyproject.toml index baf707c68..28de6a1d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,9 @@ target-version = "py38" extend-select = [ "C90", # mccabe # "N", # pep8-naming + "F", # pyflakes + "E", # pycodestyle + "W", # pycodestyle "UP", # pyupgrade "TID", # flake8-tidy-imports # "EXE", # flake8-executable diff --git a/requirements-dev.txt b/requirements-dev.txt index 3324c11e9..cd4c96eea 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,10 +7,10 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.259 -mypy==1.1.1 -pre-commit==3.2.1 -pytest==7.2.2 +ruff==0.0.262 +mypy==1.2.0 +pre-commit==3.2.2 +pytest==7.3.1 pytest-asyncio==0.21.0 pytest-cov==4.0.0 pytest-mock==3.10.0 @@ -19,14 +19,14 @@ isort==5.12.0 # For datetime mocking time-machine==2.9.0 # fastapi testing -httpx==0.23.3 +httpx==0.24.0 # Convert jupyter notebooks to markdown documents -nbconvert==7.2.10 +nbconvert==7.3.1 # mypy types -types-cachetools==5.3.0.4 +types-cachetools==5.3.0.5 types-filelock==3.2.7 -types-requests==2.28.11.16 -types-tabulate==0.9.0.1 -types-python-dateutil==2.8.19.10 +types-requests==2.28.11.17 +types-tabulate==0.9.0.2 +types-python-dateutil==2.8.19.12 diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index 4de7d8fab..f4e1e557b 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -8,3 +8,5 @@ sb3-contrib==1.7.0; python_version < '3.11' # Gym is forced to this version by stable-baselines3. setuptools==65.5.1 # Should be removed when gym is fixed. gym==0.21; python_version < '3.11' +# Progress bar for stable-baselines3 and sb3-contrib +tqdm==4.65.0; python_version < '3.11' diff --git a/requirements-freqai.txt b/requirements-freqai.txt index e6eae667c..51396ab91 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -7,5 +7,5 @@ scikit-learn==1.1.3 joblib==1.2.0 catboost==1.1.1; platform_machine != 'aarch64' and 'arm' not in platform_machine and python_version < '3.11' lightgbm==3.3.5 -xgboost==1.7.4 -tensorboard==2.12.0 +xgboost==1.7.5 +tensorboard==2.12.2 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 2c7c27d98..87b1fd3c8 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,5 +5,4 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.10.6 -progressbar2==4.2.0 +filelock==3.12.0 diff --git a/requirements-plot.txt b/requirements-plot.txt index ad7bade95..8b9ad5bc4 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,4 +1,4 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.13.1 +plotly==5.14.1 diff --git a/requirements.txt b/requirements.txt index b888d9f6e..9d51852fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,18 @@ -numpy==1.24.2 +numpy==1.24.3 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==3.0.37 -cryptography==40.0.1 +ccxt==3.0.75 +cryptography==40.0.2 aiohttp==3.8.4 -SQLAlchemy==2.0.7 +SQLAlchemy==2.0.10 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 requests==2.28.2 urllib3==1.26.15 jsonschema==4.17.3 -TA-Lib==0.4.25 +TA-Lib==0.4.26 technical==1.4.0 tabulate==0.9.0 pycoingecko==3.1.0 @@ -20,6 +20,7 @@ jinja2==3.1.2 tables==3.8.0 blosc==1.11.1 joblib==1.2.0 +rich==13.3.4 pyarrow==11.0.0; platform_machine != 'armv7l' # find first, C search in arrays @@ -28,18 +29,18 @@ py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.10 # Properly format api responses -orjson==3.8.8 +orjson==3.8.10 # Notify systemd sdnotify==0.3.2 # API Server -fastapi==0.95.0 +fastapi==0.95.1 pydantic==1.10.7 uvicorn==0.21.1 pyjwt==2.6.0 aiofiles==23.1.0 -psutil==5.9.4 +psutil==5.9.5 # Support for colorized terminal output colorama==0.4.6 @@ -50,10 +51,10 @@ prompt-toolkit==3.0.38 python-dateutil==2.8.2 #Futures -schedule==1.1.0 +schedule==1.2.0 #WS Messages -websockets==10.4 +websockets==11.0.2 janus==1.0.0 ast-comments==1.0.1 diff --git a/setup.py b/setup.py index edd7b243b..048dc066d 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,6 @@ hyperopt = [ 'scikit-learn', 'scikit-optimize>=0.7.0', 'filelock', - 'progressbar2', ] freqai = [ @@ -59,7 +58,7 @@ setup( install_requires=[ # from requirements.txt 'ccxt>=2.6.26', - 'SQLAlchemy', + 'SQLAlchemy>=2.0.6', 'python-telegram-bot>=13.4', 'arrow>=0.17.0', 'cachetools', @@ -82,6 +81,7 @@ setup( 'numpy', 'pandas', 'joblib>=1.2.0', + 'rich', 'pyarrow; platform_machine != "armv7l"', 'fastapi', 'pydantic>=1.8.0', diff --git a/setup.sh b/setup.sh index a9ff36536..d46569a53 100755 --- a/setup.sh +++ b/setup.sh @@ -50,7 +50,7 @@ function updateenv() { SYS_ARCH=$(uname -m) echo "pip install in-progress. Please wait..." # Setuptools 65.5.0 is the last version that can install gym==0.21.0 - ${PYTHON} -m pip install --upgrade pip wheel setuptools==65.5.1 + ${PYTHON} -m pip install --upgrade pip==23.0.1 wheel==0.38.4 setuptools==65.5.1 REQUIREMENTS_HYPEROPT="" REQUIREMENTS_PLOT="" REQUIREMENTS_FREQAI="" @@ -85,7 +85,7 @@ function updateenv() { if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS_FREQAI="-r requirements-freqai.txt --use-pep517" - read -p "Do you also want dependencies for freqai-rl (~700mb additional space required) [y/N]? " + read -p "Do you also want dependencies for freqai-rl or PyTorch (~700mb additional space required) [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS_FREQAI="-r requirements-freqai-rl.txt" diff --git a/tests/conftest.py b/tests/conftest.py index 0aa6e70a8..2f2345d54 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -504,7 +504,7 @@ def get_default_conf(testdatadir): {"method": "StaticPairList"} ], "telegram": { - "enabled": True, + "enabled": False, "token": "token", "chat_id": "0", "notification_settings": {}, diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index fda33b859..d44dae00d 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -48,7 +48,7 @@ def test_create_stoploss_order_binance(default_conf, mocker, limitratio, expecte default_conf['margin_mode'] = MarginMode.ISOLATED default_conf['trading_mode'] = trademode mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') @@ -127,7 +127,7 @@ def test_create_stoploss_order_dry_run_binance(default_conf, mocker): order_type = 'stop_loss_limit' default_conf['dry_run'] = True mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 4a65b16d7..60855ca54 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -528,9 +528,11 @@ class TestCCXTExchange(): assert res[1] == timeframe assert res[2] == candle_type candles = res[3] - candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * 0.9 - candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms - assert len(candles) >= min(candle_count, candle_count1) + factor = 0.9 + candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * factor + candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms * factor + assert len(candles) >= min(candle_count, candle_count1), \ + f"{len(candles)} < {candle_count} in {timeframe}, Offset: {offset} {factor}" assert candles[0][0] == since_ms or (since_ms + timeframe_ms) def test_ccxt__async_get_candle_history(self, exchange: EXCHANGE_FIXTURE_TYPE): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e08815e61..b0760944a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -8,6 +8,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch import arrow import ccxt import pytest +from ccxt import DECIMAL_PLACES, ROUND, ROUND_UP, TICK_SIZE, TRUNCATE from pandas import DataFrame from freqtrade.enums import CandleType, MarginMode, TradingMode @@ -315,35 +316,54 @@ def test_amount_to_precision(amount, precision_mode, precision, expected,): assert amount_to_precision(amount, precision, precision_mode) == expected -@pytest.mark.parametrize("price,precision_mode,precision,expected", [ - (2.34559, 2, 4, 2.3456), - (2.34559, 2, 5, 2.34559), - (2.34559, 2, 3, 2.346), - (2.9999, 2, 3, 3.000), - (2.9909, 2, 3, 2.991), - # Tests for Tick_size - (2.34559, 4, 0.0001, 2.3456), - (2.34559, 4, 0.00001, 2.34559), - (2.34559, 4, 0.001, 2.346), - (2.9999, 4, 0.001, 3.000), - (2.9909, 4, 0.001, 2.991), - (2.9909, 4, 0.005, 2.995), - (2.9973, 4, 0.005, 3.0), - (2.9977, 4, 0.005, 3.0), - (234.43, 4, 0.5, 234.5), - (234.53, 4, 0.5, 235.0), - (0.891534, 4, 0.0001, 0.8916), - (64968.89, 4, 0.01, 64968.89), - (0.000000003483, 4, 1e-12, 0.000000003483), - +@pytest.mark.parametrize("price,precision_mode,precision,expected,rounding_mode", [ + # Tests for DECIMAL_PLACES, ROUND_UP + (2.34559, 2, 4, 2.3456, ROUND_UP), + (2.34559, 2, 5, 2.34559, ROUND_UP), + (2.34559, 2, 3, 2.346, ROUND_UP), + (2.9999, 2, 3, 3.000, ROUND_UP), + (2.9909, 2, 3, 2.991, ROUND_UP), + # Tests for DECIMAL_PLACES, ROUND + (2.345600000000001, DECIMAL_PLACES, 4, 2.3456, ROUND), + (2.345551, DECIMAL_PLACES, 4, 2.3456, ROUND), + (2.49, DECIMAL_PLACES, 0, 2., ROUND), + (2.51, DECIMAL_PLACES, 0, 3., ROUND), + (5.1, DECIMAL_PLACES, -1, 10., ROUND), + (4.9, DECIMAL_PLACES, -1, 0., ROUND), + # Tests for TICK_SIZE, ROUND_UP + (2.34559, TICK_SIZE, 0.0001, 2.3456, ROUND_UP), + (2.34559, TICK_SIZE, 0.00001, 2.34559, ROUND_UP), + (2.34559, TICK_SIZE, 0.001, 2.346, ROUND_UP), + (2.9999, TICK_SIZE, 0.001, 3.000, ROUND_UP), + (2.9909, TICK_SIZE, 0.001, 2.991, ROUND_UP), + (2.9909, TICK_SIZE, 0.005, 2.995, ROUND_UP), + (2.9973, TICK_SIZE, 0.005, 3.0, ROUND_UP), + (2.9977, TICK_SIZE, 0.005, 3.0, ROUND_UP), + (234.43, TICK_SIZE, 0.5, 234.5, ROUND_UP), + (234.53, TICK_SIZE, 0.5, 235.0, ROUND_UP), + (0.891534, TICK_SIZE, 0.0001, 0.8916, ROUND_UP), + (64968.89, TICK_SIZE, 0.01, 64968.89, ROUND_UP), + (0.000000003483, TICK_SIZE, 1e-12, 0.000000003483, ROUND_UP), + # Tests for TICK_SIZE, ROUND + (2.49, TICK_SIZE, 1., 2., ROUND), + (2.51, TICK_SIZE, 1., 3., ROUND), + (2.000000051, TICK_SIZE, 0.0000001, 2.0000001, ROUND), + (2.000000049, TICK_SIZE, 0.0000001, 2., ROUND), + (2.9909, TICK_SIZE, 0.005, 2.990, ROUND), + (2.9973, TICK_SIZE, 0.005, 2.995, ROUND), + (2.9977, TICK_SIZE, 0.005, 3.0, ROUND), + (234.24, TICK_SIZE, 0.5, 234., ROUND), + (234.26, TICK_SIZE, 0.5, 234.5, ROUND), + # Tests for TRUNCATTE + (2.34559, 2, 4, 2.3455, TRUNCATE), + (2.34559, 2, 5, 2.34559, TRUNCATE), + (2.34559, 2, 3, 2.345, TRUNCATE), + (2.9999, 2, 3, 2.999, TRUNCATE), + (2.9909, 2, 3, 2.990, TRUNCATE), ]) -def test_price_to_precision(price, precision_mode, precision, expected): - # digits counting mode - # DECIMAL_PLACES = 2 - # SIGNIFICANT_DIGITS = 3 - # TICK_SIZE = 4 - - assert price_to_precision(price, precision, precision_mode) == expected +def test_price_to_precision(price, precision_mode, precision, expected, rounding_mode): + assert price_to_precision( + price, precision, precision_mode, rounding_mode=rounding_mode) == expected @pytest.mark.parametrize("price,precision_mode,precision,expected", [ @@ -417,7 +437,7 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: } mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result = 2 * 2 * (1 + 0.05) / (1 - abs(stoploss)) + expected_result = 2 * 2 * (1 + 0.05) assert pytest.approx(result) == expected_result # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) @@ -426,14 +446,14 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) assert result == 20000 - # min amount and cost are set (cost is minimal) + # min amount and cost are set (cost is minimal and therefore ignored) markets["ETH/BTC"]["limits"] = { 'cost': {'min': 2, 'max': None}, 'amount': {'min': 2, 'max': None}, } mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result = max(2, 2 * 2) * (1 + 0.05) / (1 - abs(stoploss)) + expected_result = max(2, 2 * 2) * (1 + 0.05) assert pytest.approx(result) == expected_result # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) @@ -476,6 +496,9 @@ def test__get_stake_amount_limit(mocker, default_conf) -> None: result = exchange.get_max_pair_stake_amount('ETH/BTC', 2) assert result == 1000 + result = exchange.get_max_pair_stake_amount('ETH/BTC', 2, 12.0) + assert result == 1000 / 12 + markets["ETH/BTC"]["contractSize"] = '0.01' default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated' @@ -1232,9 +1255,10 @@ def test_create_dry_run_order_fees( ("buy", 29.563, True, True), ("sell", 21.563, True, True), ]) +@pytest.mark.parametrize("leverage", [1, 2, 5]) @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, filled, caplog, - exchange_name, order_book_l2_usd, converted): + exchange_name, order_book_l2_usd, converted, leverage): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) mocker.patch.multiple(EXMS, @@ -1248,7 +1272,7 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill side=side, amount=1, rate=price, - leverage=1.0 + leverage=leverage, ) assert order_book_l2_usd.call_count == 1 assert 'id' in order @@ -1272,6 +1296,7 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill assert order_book_l2_usd.call_count == (1 if not filled else 0) assert order_closed['status'] == ('open' if not filled else 'closed') assert order_closed['filled'] == (0 if not filled else 1) + assert order_closed['cost'] == 1 * order_closed['average'] order_book_l2_usd.reset_mock() @@ -1294,9 +1319,10 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, price, fill ("sell", 25.564, 1000, 25.5555), # More than orderbook return ("sell", 27, 10000, 25.65), # max-slippage 5% ]) +@pytest.mark.parametrize("leverage", [1, 2, 5]) @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amount, endprice, - exchange_name, order_book_l2_usd): + exchange_name, order_book_l2_usd, leverage): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) mocker.patch.multiple(EXMS, @@ -1310,7 +1336,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou side=side, amount=amount, rate=rate, - leverage=1.0 + leverage=leverage, ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] @@ -1319,6 +1345,8 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou assert order["symbol"] == "LTC/USDT" assert order['status'] == 'closed' assert order['filled'] == amount + assert order['amount'] == amount + assert pytest.approx(order['cost']) == amount * order['average'] assert round(order["average"], 4) == round(endprice, 4) @@ -5281,7 +5309,7 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun }) default_conf['dry_run'] = False mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange.get_contract_size = MagicMock(return_value=contract_size) @@ -5301,3 +5329,10 @@ def test_stoploss_contract_size(mocker, default_conf, contract_size, order_amoun assert order['cost'] == 100 assert order['filled'] == 100 assert order['remaining'] == 100 + + +def test_price_to_precision_with_default_conf(default_conf, mocker): + conf = copy.deepcopy(default_conf) + patched_ex = get_patched_exchange(mocker, conf) + prec_price = patched_ex.price_to_precision("XRP/USDT", 1.0000000101) + assert prec_price == 1.00000001 diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py index 85d2ced9d..8be8ef8b3 100644 --- a/tests/exchange/test_huobi.py +++ b/tests/exchange/test_huobi.py @@ -27,7 +27,7 @@ def test_create_stoploss_order_huobi(default_conf, mocker, limitratio, expected, }) default_conf['dry_run'] = False mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') @@ -80,7 +80,7 @@ def test_create_stoploss_order_dry_run_huobi(default_conf, mocker): order_type = 'stop-limit' default_conf['dry_run'] = True mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi') diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 40a5a5b38..8fc23b94e 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -29,7 +29,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): default_conf['dry_run'] = False mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") order = exchange.create_order( @@ -192,7 +192,7 @@ def test_create_stoploss_order_kraken(default_conf, mocker, ordertype, side, adj default_conf['dry_run'] = False mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') @@ -263,7 +263,7 @@ def test_create_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py index 07f3fb6a3..741ee27be 100644 --- a/tests/exchange/test_kucoin.py +++ b/tests/exchange/test_kucoin.py @@ -27,7 +27,7 @@ def test_create_stoploss_order_kucoin(default_conf, mocker, limitratio, expected }) default_conf['dry_run'] = False mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') if order_type == 'limit': @@ -88,7 +88,7 @@ def test_stoploss_order_dry_run_kucoin(default_conf, mocker): order_type = 'market' default_conf['dry_run'] = True mocker.patch(f'{EXMS}.amount_to_precision', lambda s, x, y: y) - mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y: y) + mocker.patch(f'{EXMS}.price_to_precision', lambda s, x, y, **kwargs: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 7a3fa22f0..3824eddb7 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -1,14 +1,14 @@ from datetime import datetime, timedelta, timezone from pathlib import Path -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock import ccxt import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode -from freqtrade.exceptions import RetryableOrderError +from freqtrade.exceptions import RetryableOrderError, TemporaryError from freqtrade.exchange.exchange import timeframe_to_minutes -from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has +from tests.conftest import EXMS, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -278,7 +278,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, 'fetchLeverageTiers': False, 'fetchMarketLeverageTiers': True, }) - api_mock.fetch_market_leverage_tiers = get_mock_coro(side_effect=[ + api_mock.fetch_market_leverage_tiers = AsyncMock(side_effect=[ [ { 'tier': 1, @@ -341,6 +341,7 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, } }, ], + TemporaryError("this Failed"), [ { 'tier': 1, diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index e140ee80b..ab4a62a9e 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -1,5 +1,6 @@ from copy import deepcopy from pathlib import Path +from typing import Any, Dict from unittest.mock import MagicMock import pytest @@ -85,6 +86,22 @@ def make_rl_config(conf): return conf +def mock_pytorch_mlp_model_training_parameters() -> Dict[str, Any]: + return { + "learning_rate": 3e-4, + "trainer_kwargs": { + "max_iters": 1, + "batch_size": 64, + "max_n_eval_batches": 1, + }, + "model_kwargs": { + "hidden_dim": 32, + "dropout_percent": 0.2, + "n_layer": 1, + } + } + + def get_patched_data_kitchen(mocker, freqaiconf): dk = FreqaiDataKitchen(freqaiconf) return dk @@ -119,6 +136,7 @@ def make_unfiltered_dataframe(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.pair = "ADA/BTC" data_load_timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) @@ -152,6 +170,7 @@ def make_data_dictionary(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.pair = "ADA/BTC" data_load_timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(data_load_timerange, freqai.dk) diff --git a/tests/freqai/test_freqai_datadrawer.py b/tests/freqai/test_freqai_datadrawer.py index da3b8f9c1..8ab2c75da 100644 --- a/tests/freqai/test_freqai_datadrawer.py +++ b/tests/freqai/test_freqai_datadrawer.py @@ -19,6 +19,7 @@ def test_update_historic_data(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -41,6 +42,7 @@ def test_load_all_pairs_histories(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -60,6 +62,7 @@ def test_get_base_and_corr_dataframes(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180111-20180114") @@ -87,6 +90,7 @@ def test_use_strategy_to_populate_indicators(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180114") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180111-20180114") @@ -103,8 +107,9 @@ def test_get_timerange_from_live_historic_predictions(mocker, freqai_conf): exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) freqai = strategy.freqai - freqai.live = True + freqai.live = False freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = False timerange = TimeRange.parse_timerange("20180126-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) sub_timerange = TimeRange.parse_timerange("20180128-20180130") diff --git a/tests/freqai/test_freqai_datakitchen.py b/tests/freqai/test_freqai_datakitchen.py index 95665a775..3f0fc697d 100644 --- a/tests/freqai/test_freqai_datakitchen.py +++ b/tests/freqai/test_freqai_datakitchen.py @@ -180,6 +180,7 @@ def test_get_full_model_path(mocker, freqai_conf, model): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 3b370aea4..7346191db 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -15,7 +15,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re -from tests.freqai.conftest import get_patched_freqai_strategy, make_rl_config +from tests.freqai.conftest import (get_patched_freqai_strategy, make_rl_config, + mock_pytorch_mlp_model_training_parameters) def is_py11() -> bool: @@ -34,13 +35,14 @@ def is_mac() -> bool: def can_run_model(model: str) -> None: if (is_arm() or is_py11()) and "Catboost" in model: - pytest.skip("CatBoost is not supported on ARM") + pytest.skip("CatBoost is not supported on ARM.") - if is_mac() and not is_arm() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning module not available on intel based Mac OS") + is_pytorch_model = 'Reinforcement' in model or 'PyTorch' in model + if is_pytorch_model and is_mac() and not is_arm(): + pytest.skip("Reinforcement learning / PyTorch module not available on intel based Mac OS.") - if is_py11() and 'Reinforcement' in model: - pytest.skip("Reinforcement learning currently not available on python 3.11.") + if is_pytorch_model and is_py11(): + pytest.skip("Reinforcement learning / PyTorch currently not available on python 3.11.") @pytest.mark.parametrize('model, pca, dbscan, float32, can_short, shuffle, buffer', [ @@ -48,11 +50,12 @@ def can_run_model(model: str) -> None: ('XGBoostRegressor', False, True, False, True, False, 10), ('XGBoostRFRegressor', False, False, False, True, False, 0), ('CatboostRegressor', False, False, False, True, True, 0), + ('PyTorchMLPRegressor', False, False, False, True, False, 0), ('ReinforcementLearner', False, True, False, True, False, 0), ('ReinforcementLearner_multiproc', False, False, False, True, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, False, False, 0), ('ReinforcementLearner_test_3ac', False, False, False, True, False, 0), - ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0) + ('ReinforcementLearner_test_4ac', False, False, False, True, False, 0), ]) def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, dbscan, float32, can_short, shuffle, buffer): @@ -79,6 +82,11 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") freqai_conf["freqai"]["rl_config"]["drop_ohlc_from_features"] = True + if 'PyTorchMLPRegressor' in model: + model_save_ext = 'zip' + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + strategy = get_patched_freqai_strategy(mocker, freqai_conf) exchange = get_patched_exchange(mocker, freqai_conf) strategy.dp = DataProvider(freqai_conf, exchange) @@ -87,6 +95,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, freqai.live = True freqai.can_short = can_short freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True freqai.dk.set_paths('ADA/BTC', 10000) timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -122,8 +131,7 @@ def test_extract_data_and_train_model_Standard(mocker, freqai_conf, model, pca, ('CatboostClassifierMultiTarget', "freqai_test_multimodel_classifier_strat") ]) def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, strat): - if (is_arm() or is_py11()) and 'Catboost' in model: - pytest.skip("CatBoost is not supported on ARM") + can_run_model(model) freqai_conf.update({"timerange": "20180110-20180130"}) freqai_conf.update({"strategy": strat}) @@ -135,6 +143,7 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -162,10 +171,10 @@ def test_extract_data_and_train_model_MultiTargets(mocker, freqai_conf, model, s 'CatboostClassifier', 'XGBoostClassifier', 'XGBoostRFClassifier', + 'PyTorchMLPClassifier', ]) def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): - if (is_arm() or is_py11()) and model == 'CatboostClassifier': - pytest.skip("CatBoost is not supported on ARM") + can_run_model(model) freqai_conf.update({"freqaimodel": model}) freqai_conf.update({"strategy": "freqai_test_classifier"}) @@ -178,6 +187,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -190,7 +200,20 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): freqai.extract_data_and_train_model(new_timerange, "ADA/BTC", strategy, freqai.dk, data_load_timerange) - assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_model.joblib").exists() + if 'PyTorchMLPClassifier': + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + + if freqai.dd.model_type == 'joblib': + model_file_extension = ".joblib" + elif freqai.dd.model_type == "pytorch": + model_file_extension = ".zip" + else: + raise Exception(f"Unsupported model type: {freqai.dd.model_type}," + f" can't assign model_file_extension") + + assert Path(freqai.dk.data_path / + f"{freqai.dk.model_filename}_model{model_file_extension}").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_metadata.json").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_trained_df.pkl").exists() assert Path(freqai.dk.data_path / f"{freqai.dk.model_filename}_svm_model.joblib").exists() @@ -204,10 +227,12 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): ("LightGBMRegressor", 2, "freqai_test_strat"), ("XGBoostRegressor", 2, "freqai_test_strat"), ("CatboostRegressor", 2, "freqai_test_strat"), + ("PyTorchMLPRegressor", 2, "freqai_test_strat"), ("ReinforcementLearner", 3, "freqai_rl_test_strat"), ("XGBoostClassifier", 2, "freqai_test_classifier"), ("LightGBMClassifier", 2, "freqai_test_classifier"), - ("CatboostClassifier", 2, "freqai_test_classifier") + ("CatboostClassifier", 2, "freqai_test_classifier"), + ("PyTorchMLPClassifier", 2, "freqai_test_classifier") ], ) def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog): @@ -228,6 +253,10 @@ def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog) if 'test_4ac' in model: freqai_conf["freqaimodel_path"] = str(Path(__file__).parents[1] / "freqai" / "test_models") + if 'PyTorchMLP' in model: + pytorch_mlp_mtp = mock_pytorch_mlp_model_training_parameters() + freqai_conf['freqai']['model_training_parameters'].update(pytorch_mlp_mtp) + freqai_conf.get("freqai", {}).get("feature_parameters", {}).update( {"indicator_periods_candles": [2]}) @@ -371,6 +400,9 @@ def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog): sub_timerange = TimeRange.parse_timerange("20180129-20180130") corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk) df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC") + df = strategy.set_freqai_targets(df.copy(), metadata={"pair": "LTC/BTC"}) + df = freqai.dk.remove_special_chars_from_feature_names(df) + freqai.dk.get_unique_classes_from_labels(df) freqai.dk.pair = "ADA/BTC" freqai.dk.full_df = df.fillna(0) freqai.dk.full_df @@ -394,6 +426,7 @@ def test_principal_component_analysis(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) @@ -425,10 +458,12 @@ def test_plot_feature_importance(mocker, freqai_conf): freqai = strategy.freqai freqai.live = True freqai.dk = FreqaiDataKitchen(freqai_conf) + freqai.dk.live = True timerange = TimeRange.parse_timerange("20180110-20180130") freqai.dd.load_all_pair_histories(timerange, freqai.dk) - freqai.dd.pair_dict = MagicMock() + freqai.dd.pair_dict = {"ADA/BTC": {"model_filename": "fake_name", + "trained_timestamp": 1, "data_path": "", "extras": {}}} data_load_timerange = TimeRange.parse_timerange("20180110-20180130") new_timerange = TimeRange.parse_timerange("20180120-20180130") diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 0cc32baaf..6428177c5 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -9,7 +9,7 @@ import pytest from arrow import Arrow from freqtrade.configuration import TimeRange -from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN +from freqtrade.constants import BACKTEST_BREAKDOWNS, DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data import history from freqtrade.data.btanalysis import (get_latest_backtest_filename, load_backtest_data, load_backtest_stats) @@ -465,11 +465,14 @@ def test_generate_periodic_breakdown_stats(testdatadir): def test__get_resample_from_period(): assert _get_resample_from_period('day') == '1d' - assert _get_resample_from_period('week') == '1w' + assert _get_resample_from_period('week') == '1W-MON' assert _get_resample_from_period('month') == '1M' with pytest.raises(ValueError, match=r"Period noooo is not supported."): _get_resample_from_period('noooo') + for period in BACKTEST_BREAKDOWNS: + assert isinstance(_get_resample_from_period(period), str) + def test_show_sorted_pairlist(testdatadir, default_conf, capsys): filename = testdatadir / "backtest_results/backtest-result.json" diff --git a/tests/persistence/test_key_value_store.py b/tests/persistence/test_key_value_store.py new file mode 100644 index 000000000..1dab8764a --- /dev/null +++ b/tests/persistence/test_key_value_store.py @@ -0,0 +1,69 @@ +from datetime import datetime, timedelta, timezone + +import pytest + +from freqtrade.persistence.key_value_store import KeyValueStore, set_startup_time +from tests.conftest import create_mock_trades_usdt + + +@pytest.mark.usefixtures("init_persistence") +def test_key_value_store(time_machine): + start = datetime(2023, 1, 1, 4, tzinfo=timezone.utc) + time_machine.move_to(start, tick=False) + + KeyValueStore.store_value("test", "testStringValue") + KeyValueStore.store_value("test_dt", datetime.now(timezone.utc)) + KeyValueStore.store_value("test_float", 22.51) + KeyValueStore.store_value("test_int", 15) + + assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_value("test") == "testStringValue" + assert KeyValueStore.get_string_value("test") == "testStringValue" + assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_datetime_value("test_dt") == datetime.now(timezone.utc) + assert KeyValueStore.get_string_value("test_dt") is None + assert KeyValueStore.get_float_value("test_dt") is None + assert KeyValueStore.get_int_value("test_dt") is None + assert KeyValueStore.get_value("test_float") == 22.51 + assert KeyValueStore.get_float_value("test_float") == 22.51 + assert KeyValueStore.get_value("test_int") == 15 + assert KeyValueStore.get_int_value("test_int") == 15 + assert KeyValueStore.get_datetime_value("test_int") is None + + time_machine.move_to(start + timedelta(days=20, hours=5), tick=False) + assert KeyValueStore.get_value("test_dt") != datetime.now(timezone.utc) + assert KeyValueStore.get_value("test_dt") == start + # Test update works + KeyValueStore.store_value("test_dt", datetime.now(timezone.utc)) + assert KeyValueStore.get_value("test_dt") == datetime.now(timezone.utc) + + KeyValueStore.store_value("test_float", 23.51) + assert KeyValueStore.get_value("test_float") == 23.51 + # test deleting + KeyValueStore.delete_value("test_float") + assert KeyValueStore.get_value("test_float") is None + # Delete same value again (should not fail) + KeyValueStore.delete_value("test_float") + + with pytest.raises(ValueError, match=r"Unknown value type"): + KeyValueStore.store_value("test_float", {'some': 'dict'}) + + +@pytest.mark.usefixtures("init_persistence") +def test_set_startup_time(fee, time_machine): + create_mock_trades_usdt(fee) + start = datetime.now(timezone.utc) + time_machine.move_to(start, tick=False) + set_startup_time() + + assert KeyValueStore.get_value("startup_time") == start + initial_time = KeyValueStore.get_value("bot_start_time") + assert initial_time <= start + + # Simulate bot restart + new_start = start + timedelta(days=5) + time_machine.move_to(new_start, tick=False) + set_startup_time() + + assert KeyValueStore.get_value("startup_time") == new_start + assert KeyValueStore.get_value("bot_start_time") == initial_time diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 23ec6d4fb..948973ed5 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -6,7 +6,7 @@ import arrow import pytest from sqlalchemy import select -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH, DATETIME_PRINT_FORMAT from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException from freqtrade.persistence import LocalTrade, Order, Trade, init_db @@ -2037,6 +2037,7 @@ def test_Trade_object_idem(): 'get_mix_tag_performance', 'get_trading_volume', 'from_json', + 'validate_string_len', ) EXCLUDES2 = ('trades', 'trades_open', 'bt_trades_open_pp', 'bt_open_open_trade_count', 'total_profit') @@ -2055,6 +2056,31 @@ def test_Trade_object_idem(): assert item in trade +@pytest.mark.usefixtures("init_persistence") +def test_trade_truncates_string_fields(): + trade = Trade( + pair='ADA/USDT', + stake_amount=20.0, + amount=30.0, + open_rate=2.0, + open_date=datetime.utcnow() - timedelta(minutes=20), + fee_open=0.001, + fee_close=0.001, + exchange='binance', + leverage=1.0, + trading_mode='futures', + enter_tag='a' * CUSTOM_TAG_MAX_LENGTH * 2, + exit_reason='b' * CUSTOM_TAG_MAX_LENGTH * 2, + ) + Trade.session.add(trade) + Trade.commit() + + trade1 = Trade.session.scalars(select(Trade)).first() + + assert trade1.enter_tag == 'a' * CUSTOM_TAG_MAX_LENGTH + assert trade1.exit_reason == 'b' * CUSTOM_TAG_MAX_LENGTH + + def test_recalc_trade_from_orders(fee): o1_amount = 100 diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ff08a0564..5a84eaa48 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -591,6 +591,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): 'side': 'short', } ] + assert result['starting_capital'] == 10 + assert result['starting_capital_ratio'] == 0.0 def test_rpc_start(mocker, default_conf) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 31075e514..58c904838 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -883,6 +883,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected) 'max_drawdown': ANY, 'max_drawdown_abs': ANY, 'trading_volume': expected['trading_volume'], + 'bot_start_timestamp': 0, + 'bot_start_date': '', } @@ -1403,10 +1405,10 @@ def test_api_pair_candles(botclient, ohlcv_history): ]) -def test_api_pair_history(botclient, ohlcv_history): +def test_api_pair_history(botclient, mocker): ftbot, client = botclient timeframe = '5m' - + lfm = mocker.patch('freqtrade.strategy.interface.IStrategy.load_freqAI_model') # No pair rc = client_get(client, f"{BASE_URI}/pair_history?timeframe={timeframe}" @@ -1440,6 +1442,7 @@ def test_api_pair_history(botclient, ohlcv_history): assert len(rc.json()['data']) == rc.json()['length'] assert 'columns' in rc.json() assert 'data' in rc.json() + assert lfm.call_count == 1 assert rc.json()['pair'] == 'UNITTEST/BTC' assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 21c8b0813..f0bb72fc9 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -28,6 +28,7 @@ def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) + default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) @@ -52,6 +53,7 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) + default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) @@ -85,7 +87,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', side_effect=ValueError()) - + default_conf['telegram']['enabled'] = True freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ @@ -99,6 +101,7 @@ def test_send_msg_telegram_error(mocker, default_conf, caplog) -> None: def test_process_msg_queue(mocker, default_conf, caplog) -> None: telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') + default_conf['telegram']['enabled'] = True default_conf['telegram']['allow_custom_messages'] = True mocker.patch('freqtrade.rpc.telegram.Telegram._init') @@ -115,9 +118,9 @@ def test_process_msg_queue(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: + default_conf['telegram']['enabled'] = True telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg') mocker.patch('freqtrade.rpc.telegram.Telegram._init') - freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ @@ -166,7 +169,8 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non caplog) -def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: +def test_startupmessages_telegram_enabled(mocker, default_conf) -> None: + default_conf['telegram']['enabled'] = True telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 54f612c59..7978a2a23 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -36,6 +36,13 @@ from tests.conftest import (CURRENT_TEST_STRATEGY, EXMS, create_mock_trades, patch_exchange, patch_get_signal, patch_whitelist) +@pytest.fixture +def default_conf(default_conf) -> dict: + # Telegram is enabled by default + default_conf['telegram']['enabled'] = True + return default_conf + + class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator @@ -2241,8 +2248,9 @@ def test_send_msg_buy_notification_no_fiat( ('Short', 'short_signal_01', 2.0), ]) def test_send_msg_sell_notification_no_fiat( - default_conf, mocker, direction, enter_signal, leverage) -> None: + default_conf, mocker, direction, enter_signal, leverage, time_machine) -> None: del default_conf['fiat_display_currency'] + time_machine.move_to('2022-05-02 00:00:00 +00:00', tick=False) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ diff --git a/tests/strategy/strats/freqai_test_classifier.py b/tests/strategy/strats/freqai_test_classifier.py index 61b9f0c37..a68a87b2a 100644 --- a/tests/strategy/strats/freqai_test_classifier.py +++ b/tests/strategy/strats/freqai_test_classifier.py @@ -82,7 +82,7 @@ class freqai_test_classifier(IStrategy): return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: Dict, **kwargs): - + self.freqai.class_names = ["down", "up"] dataframe['&s-up_or_down'] = np.where(dataframe["close"].shift(-100) > dataframe["close"], 'up', 'down') diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 7b1399507..204fa996d 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -9,6 +9,7 @@ import pytest from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.constants import CUSTOM_TAG_MAX_LENGTH from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.enums import ExitCheckTuple, ExitType, HyperoptState, SignalDirection @@ -529,13 +530,13 @@ def test_custom_exit(default_conf, fee, caplog) -> None: assert res[0].exit_reason == 'hello world' caplog.clear() - strategy.custom_exit = MagicMock(return_value='h' * 100) + strategy.custom_exit = MagicMock(return_value='h' * CUSTOM_TAG_MAX_LENGTH * 2) res = strategy.should_exit(trade, 1, now, enter=False, exit_=False, low=None, high=None) assert res[0].exit_type == ExitType.CUSTOM_EXIT assert res[0].exit_flag is True - assert res[0].exit_reason == 'h' * 64 + assert res[0].exit_reason == 'h' * (CUSTOM_TAG_MAX_LENGTH) assert log_has_re('Custom exit reason returned from custom_exit is too long.*', caplog) @@ -986,7 +987,8 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): } } } - mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result) + mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params', + return_value=expected_result) PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) assert strategy.stoploss == -0.05 @@ -1005,11 +1007,13 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): } } - mocker.patch('freqtrade.strategy.hyper.json_load', return_value=expected_result) + mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params', + return_value=expected_result) with pytest.raises(OperationalException, match="Invalid parameter file provided."): StrategyResolver.load_strategy(default_conf) - mocker.patch('freqtrade.strategy.hyper.json_load', MagicMock(side_effect=ValueError())) + mocker.patch('freqtrade.strategy.hyper.HyperoptTools.load_params', + MagicMock(side_effect=ValueError())) StrategyResolver.load_strategy(default_conf) assert log_has("Invalid parameter file format.", caplog) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index aab868bec..c445b989d 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -23,7 +23,8 @@ from freqtrade.configuration.load_config import (load_config_file, load_file, lo from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from freqtrade.loggers import FTBufferingHandler, _set_loggers, setup_logging, setup_logging_pre +from freqtrade.loggers import (FTBufferingHandler, FTStdErrStreamHandler, _set_loggers, + setup_logging, setup_logging_pre) from tests.conftest import (CURRENT_TEST_STRATEGY, log_has, log_has_re, patched_configuration_load_config_file) @@ -658,7 +659,7 @@ def test_set_loggers_syslog(): setup_logging(config) assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler] - assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] assert [x for x in logger.handlers if type(x) == FTBufferingHandler] # setting up logging again should NOT cause the loggers to be added a second time. setup_logging(config) @@ -681,7 +682,7 @@ def test_set_loggers_Filehandler(tmpdir): setup_logging(config) assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler] - assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] assert [x for x in logger.handlers if type(x) == FTBufferingHandler] # setting up logging again should NOT cause the loggers to be added a second time. setup_logging(config) @@ -706,7 +707,7 @@ def test_set_loggers_journald(mocker): setup_logging(config) assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x).__name__ == "JournaldLogHandler"] - assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == FTStdErrStreamHandler] # reset handlers to not break pytest logger.handlers = orig_handlers diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 01aa730cb..7bded0f82 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -356,7 +356,7 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke @pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ (5.0, True, True, 99), - (0.049, True, False, 99), # Amount will be adjusted to min - which is 0.051 + (0.042, True, False, 99), # Amount will be adjusted to min - which is 0.051 (0, False, True, 99), (UNLIMITED_STAKE_AMOUNT, False, True, 0), ]) @@ -1290,6 +1290,137 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT) +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange_partial( + mocker, default_conf_usdt, fee, is_short, limit_order) -> None: + stop_order_dict = {'id': "101", "status": "open"} + stoploss = MagicMock(return_value=stop_order_dict) + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + create_stoploss=stoploss + ) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = None + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss.call_count == 1 + assert trade.stoploss_order_id == "101" + assert trade.amount == 30 + stop_order_dict.update({'id': "102"}) + # Stoploss on exchange is cancelled on exchange, but filled partially. + # Must update trade amount to guarantee successful exit. + stoploss_order_hit = MagicMock(return_value={ + 'id': "101", + 'status': 'canceled', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'filled': trade.amount / 2, + 'remaining': trade.amount / 2, + 'amount': enter_order['amount'], + }) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) + assert freqtrade.handle_stoploss_on_exchange(trade) is False + # Stoploss filled partially ... + assert trade.amount == 15 + + assert trade.stoploss_order_id == "102" + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange_partial_cancel_here( + mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None: + stop_order_dict = {'id': "101", "status": "open"} + default_conf_usdt['trailing_stop'] = True + stoploss = MagicMock(return_value=stop_order_dict) + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + create_stoploss=stoploss + ) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = None + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss.call_count == 1 + assert trade.stoploss_order_id == "101" + assert trade.amount == 30 + stop_order_dict.update({'id': "102"}) + # Stoploss on exchange is open. + # Freqtrade cancels the stop - but cancel returns a partial filled order. + stoploss_order_hit = MagicMock(return_value={ + 'id': "101", + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'filled': 0, + 'remaining': trade.amount, + 'amount': enter_order['amount'], + }) + stoploss_order_cancel = MagicMock(return_value={ + 'id': "101", + 'status': 'canceled', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'filled': trade.amount / 2, + 'remaining': trade.amount / 2, + 'amount': enter_order['amount'], + }) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) + mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel) + trade.stoploss_last_update = arrow.utcnow().shift(minutes=-10).datetime + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + # Canceled Stoploss filled partially ... + assert log_has_re('Cancelling current stoploss on exchange.*', caplog) + + assert trade.stoploss_order_id == "102" + assert trade.amount == 15 + + @pytest.mark.parametrize("is_short", [False, True]) def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short, limit_order) -> None: @@ -1671,7 +1802,7 @@ def test_stoploss_on_exchange_price_rounding( EXMS, get_fee=fee, ) - price_mock = MagicMock(side_effect=lambda p, s: int(s)) + price_mock = MagicMock(side_effect=lambda p, s, **kwargs: int(s)) stoploss_mock = MagicMock(return_value={'id': '13434334'}) adjust_mock = MagicMock(return_value=False) mocker.patch.multiple( @@ -2824,6 +2955,9 @@ def test_manage_open_orders_exit_usercustom( assert rpc_mock.call_count == 2 assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 + trade = Trade.session.scalars(select(Trade)).first() + # cancelling didn't succeed - order-id remains open. + assert trade.open_order_id is not None # 2nd canceled trade - Fail execute exit caplog.clear() @@ -3334,6 +3468,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: # TODO: should not be magicmock trade = MagicMock() + trade.open_order_id = '125' reason = CANCEL_REASON['TIMEOUT'] order = {'remaining': 1, 'id': '125', @@ -3341,6 +3476,10 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: 'status': "open"} assert not freqtrade.handle_cancel_exit(trade, order, reason) + # mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order) + # assert not freqtrade.handle_cancel_exit(trade, order, reason) + # assert trade.open_order_id == '125' + @pytest.mark.parametrize("is_short, open_rate, amt", [ (False, 2.0, 30.0), diff --git a/tests/test_timerange.py b/tests/test_timerange.py index 06ff1983a..993b24d95 100644 --- a/tests/test_timerange.py +++ b/tests/test_timerange.py @@ -10,6 +10,8 @@ from freqtrade.exceptions import OperationalException def test_parse_timerange_incorrect(): + timerange = TimeRange.parse_timerange('') + assert timerange == TimeRange(None, None, 0, 0) timerange = TimeRange.parse_timerange('20100522-') assert TimeRange('date', None, 1274486400, 0) == timerange assert timerange.timerange_str == '20100522-'