Merge pull request #10383 from freqtrade/new_release

New release 2024.6
This commit is contained in:
Matthias
2024-07-01 10:45:09 +02:00
committed by GitHub
94 changed files with 2967 additions and 1257 deletions
+19 -8
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ ubuntu-20.04, ubuntu-22.04 ] os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
python-version: ["3.9", "3.10", "3.11", "3.12"] python-version: ["3.9", "3.10", "3.11", "3.12"]
steps: steps:
@@ -55,7 +55,7 @@ jobs:
- name: Installation - *nix - name: Installation - *nix
run: | run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade "pip<=24.0" wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
@@ -192,7 +192,7 @@ jobs:
- name: Installation (python) - name: Installation (python)
run: | run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade "pip<=24.0" wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
@@ -318,6 +318,17 @@ jobs:
run: | run: |
mypy freqtrade scripts tests mypy freqtrade scripts tests
- name: Run Pester tests (PowerShell)
run: |
$PSVersionTable
Set-PSRepository psgallery -InstallationPolicy trusted
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
$Error.clear()
Invoke-Pester -Path "tests" -CI
if ($Error.Length -gt 0) {exit 1}
shell: powershell
- name: Discord notification - name: Discord notification
uses: rjstone/discord-webhook-notify@v1 uses: rjstone/discord-webhook-notify@v1
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
@@ -411,7 +422,7 @@ jobs:
- name: Installation - *nix - name: Installation - *nix
run: | run: |
python -m pip install --upgrade pip wheel python -m pip install --upgrade "pip<=24.0" wheel
export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
export TA_LIBRARY_PATH=${HOME}/dependencies/lib export TA_LIBRARY_PATH=${HOME}/dependencies/lib
export TA_INCLUDE_PATH=${HOME}/dependencies/include export TA_INCLUDE_PATH=${HOME}/dependencies/include
@@ -522,12 +533,12 @@ jobs:
- name: Publish to PyPI (Test) - name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.8.14 uses: pypa/gh-action-pypi-publish@v1.9.0
with: with:
repository-url: https://test.pypi.org/legacy/ repository-url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.14 uses: pypa/gh-action-pypi-publish@v1.9.0
deploy-docker: deploy-docker:
@@ -566,11 +577,11 @@ jobs:
docker version -f '{{.Server.Experimental}}' docker version -f '{{.Server.Experimental}}'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v3
- name: Available platforms - name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }} run: echo ${{ steps.buildx.outputs.platforms }}
+4 -4
View File
@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: "7.0.0" rev: "7.1.0"
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [Flake8-pyproject] additional_dependencies: [Flake8-pyproject]
@@ -16,10 +16,10 @@ repos:
additional_dependencies: additional_dependencies:
- types-cachetools==5.3.0.7 - types-cachetools==5.3.0.7
- types-filelock==3.2.7 - types-filelock==3.2.7
- types-requests==2.32.0.20240523 - types-requests==2.32.0.20240622
- types-tabulate==0.9.0.20240106 - types-tabulate==0.9.0.20240106
- types-python-dateutil==2.9.0.20240316 - types-python-dateutil==2.9.0.20240316
- SQLAlchemy==2.0.30 - SQLAlchemy==2.0.31
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
@@ -31,7 +31,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.4.5' rev: 'v0.4.10'
hooks: hooks:
- id: ruff - id: ruff
+3 -3
View File
@@ -1,4 +1,4 @@
FROM python:3.12.3-slim-bookworm as base FROM python:3.12.4-slim-bookworm as base
# Setup env # Setup env
ENV LANG C.UTF-8 ENV LANG C.UTF-8
@@ -25,7 +25,7 @@ FROM base as python-deps
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \ && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
&& apt-get clean \ && apt-get clean \
&& pip install --upgrade pip wheel && pip install --upgrade "pip<=24.0" wheel
# Install TA-lib # Install TA-lib
COPY build_helpers/* /tmp/ COPY build_helpers/* /tmp/
@@ -35,7 +35,7 @@ ENV LD_LIBRARY_PATH /usr/local/lib
# Install dependencies # Install dependencies
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/ COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
USER ftuser USER ftuser
RUN pip install --user --no-cache-dir numpy \ RUN pip install --user --no-cache-dir "numpy<2.0" \
&& pip install --user --no-cache-dir -r requirements-hyperopt.txt && pip install --user --no-cache-dir -r requirements-hyperopt.txt
# Copy dependencies to runtime-image # Copy dependencies to runtime-image
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
# vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040 # vendored Wheels compiled via https://github.com/xmatthias/ta-lib-python/tree/ta_bundled_040
python -m pip install --upgrade pip wheel python -m pip install --upgrade "pip<=24.0" wheel
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" $pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
+1 -1
View File
@@ -17,7 +17,7 @@ RUN mkdir /freqtrade \
&& chown ftuser:ftuser /freqtrade \ && chown ftuser:ftuser /freqtrade \
# Allow sudoers # Allow sudoers
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \ && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
&& pip install --upgrade pip && pip install --upgrade "pip<=24.0"
WORKDIR /freqtrade WORKDIR /freqtrade
+31 -31
View File
@@ -253,36 +253,36 @@ A backtesting result will look like that:
``` ```
================================================ BACKTESTING REPORT ================================================= ================================================ BACKTESTING REPORT =================================================
| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% | | Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|:---------|--------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:| |----------+--------+----------------+------------------+----------------+--------------+--------------------------|
| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 | | ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 | | ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 | | BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 | | DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 | | ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 | | EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 | | ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 | | ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 | | IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 | | LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 | | LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 | | NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 | | NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 | | REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 | | XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 | | XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 | | XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 | | ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 | | TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
============================================= LEFT OPEN TRADES REPORT ============================================= ============================================= LEFT OPEN TRADES REPORT =============================================
| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% | | Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|:---------|---------:|---------------:|-----------------:|---------------:|:---------------|--------------------:| |----------+---------+----------------+------------------+----------------+----------------+---------------------|
| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 | | ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 | | LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 | | TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
==================== EXIT REASON STATS ==================== ==================== EXIT REASON STATS ====================
| Exit Reason | Exits | Wins | Draws | Losses | | Exit Reason | Exits | Wins | Draws | Losses |
|:-------------------|--------:|------:|-------:|--------:| |--------------------+---------+-------+--------+---------|
| trailing_stop_loss | 205 | 150 | 0 | 55 | | trailing_stop_loss | 205 | 150 | 0 | 55 |
| stop_loss | 166 | 0 | 0 | 166 | | stop_loss | 166 | 0 | 0 | 166 |
| exit_signal | 56 | 36 | 0 | 20 | | exit_signal | 56 | 36 | 0 | 20 |
@@ -631,10 +631,10 @@ Detailed output for all strategies one after the other will be available, so mak
``` ```
================================================== STRATEGY SUMMARY =================================================================== ================================================== STRATEGY SUMMARY ===================================================================
| Strategy | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % | | Strategy | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % |
|:------------|---------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:| |-------------+---------+----------------+------------------+----------------+----------------+-------+--------+--------+------------|
| Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 | | Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 |
| Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 | | Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
``` ```
## Next step ## Next step
+1 -1
View File
@@ -373,7 +373,7 @@ Filters low-value coins which would not allow setting stoplosses.
Namely, pairs are blacklisted if a variance of one percent or more in the stop price would be caused by precision rounding on the exchange, i.e. `rounded(stop_price) <= rounded(stop_price * 0.99)`. The idea is to avoid coins with a value VERY close to their lower trading boundary, not allowing setting of proper stoploss. Namely, pairs are blacklisted if a variance of one percent or more in the stop price would be caused by precision rounding on the exchange, i.e. `rounded(stop_price) <= rounded(stop_price * 0.99)`. The idea is to avoid coins with a value VERY close to their lower trading boundary, not allowing setting of proper stoploss.
!!! Tip "PerformanceFilter is pointless for futures trading" !!! Tip "PrecisionFilter is pointless for futures trading"
The above does not apply to shorts. And for longs, in theory the trade will be liquidated first. The above does not apply to shorts. And for longs, in theory the trade will be liquidated first.
!!! Warning "Backtesting" !!! Warning "Backtesting"
+8
View File
@@ -2,6 +2,14 @@
This page explains how to plot prices, indicators and profits. This page explains how to plot prices, indicators and profits.
!!! Warning "Deprecated"
The commands described in this page (`plot-dataframe`, `plot-profit`) should be considered deprecated and are in maintenance mode.
This is mostly for the performance problems even medium sized plots can cause, but also because "store a file and open it in a browser" isn't very intuitive from a UI perspective.
While there are no immediate plans to remove them, they are not actively maintained - and may be removed short-term should major changes be required to keep them working.
Please use [FreqUI](freq-ui.md) for plotting needs, which doesn't struggle with the same performance problems.
## Installation / Setup ## Installation / Setup
Plotting modules use the Plotly library. You can install / upgrade this by running the following command: Plotting modules use the Plotly library. You can install / upgrade this by running the following command:
+1 -1
View File
@@ -1,6 +1,6 @@
markdown==3.6 markdown==3.6
mkdocs==1.6.0 mkdocs==1.6.0
mkdocs-material==9.5.24 mkdocs-material==9.5.27
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==10.8.1 pymdown-extensions==10.8.1
jinja2==3.1.4 jinja2==3.1.4
+8
View File
@@ -118,6 +118,14 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
freqtrade-client --config rest_config.json <command> [optional parameters] freqtrade-client --config rest_config.json <command> [optional parameters]
``` ```
Commands with many arguments may require keyword arguments (for clarity) - which can be provided as follows:
``` bash
freqtrade-client --config rest_config.json forceenter BTC/USDT long enter_tag=GutFeeling
```
This method will work for all arguments - check the "show" command for a list of available parameters.
??? Note "Programmatic use" ??? Note "Programmatic use"
The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API. The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API.
to do so, please use the following: to do so, please use the following:
+6 -3
View File
@@ -165,7 +165,9 @@ E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoplo
During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades). During backtesting, `current_rate` (and `current_profit`) are provided against the candle's high (or low for short trades) - while the resulting stoploss is evaluated against the candle's low (or high for short trades).
The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price.
Returning None will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss. Returning `None` will be interpreted as "no desire to change", and is the only safe way to return when you'd like to not modify the stoploss.
`NaN` and `inf` values are considered invalid and will be ignored (identical to `None`).
Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)). Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchangefreqtrade)).
@@ -467,7 +469,7 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
??? Example "Returning a stoploss using absolute price from the custom stoploss function" ??? Example "Returning a stoploss using absolute price from the custom stoploss function"
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate, is_short=trade.is_short, leverage=trade.leverage)`. If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate=current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement. For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement.
``` python ``` python
@@ -492,7 +494,8 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
candle = dataframe.iloc[-1].squeeze() candle = dataframe.iloc[-1].squeeze()
side = 1 if trade.is_short else -1 side = 1 if trade.is_short else -1
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
current_rate, is_short=trade.is_short, current_rate=current_rate,
is_short=trade.is_short,
leverage=trade.leverage) leverage=trade.leverage)
``` ```
+22 -22
View File
@@ -13,28 +13,28 @@ The following attributes / properties are available for each individual trade -
| Attribute | DataType | Description | | Attribute | DataType | Description |
|------------|-------------|-------------| |------------|-------------|-------------|
`pair`| string | Pair of this trade | `pair` | string | Pair of this trade. |
`is_open`| boolean | Is the trade currently open, or has it been concluded | `is_open` | boolean | Is the trade currently open, or has it been concluded. |
`open_rate`| float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments) | `open_rate` | float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments). |
`close_rate`| float | Close rate - only set when is_open = False | `close_rate` | float | Close rate - only set when is_open = False. |
`stake_amount`| float | Amount in Stake (or Quote) currency. | `stake_amount` | float | Amount in Stake (or Quote) currency. |
`amount`| float | Amount in Asset / Base currency that is currently owned. | `amount` | float | Amount in Asset / Base currency that is currently owned. |
`open_date`| datetime | Timestamp when trade was opened **use `open_date_utc` instead** | `open_date` | datetime | Timestamp when trade was opened **use `open_date_utc` instead** |
`open_date_utc`| datetime | Timestamp when trade was opened - in UTC | `open_date_utc` | datetime | Timestamp when trade was opened - in UTC. |
`close_date`| datetime | Timestamp when trade was closed **use `close_date_utc` instead** | `close_date` | datetime | Timestamp when trade was closed **use `close_date_utc` instead** |
`close_date_utc`| datetime | Timestamp when trade was closed - in UTC | `close_date_utc` | datetime | Timestamp when trade was closed - in UTC. |
`close_profit`| float | Relative profit at the time of trade closure. `0.01` == 1% | `close_profit` | float | Relative profit at the time of trade closure. `0.01` == 1% |
`close_profit_abs`| float | Absolute profit (in stake currency) at the time of trade closure. | `close_profit_abs` | float | Absolute profit (in stake currency) at the time of trade closure. |
`leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets. | `leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets. |
`enter_tag`| string | Tag provided on entry via the `enter_tag` column in the dataframe | `enter_tag` | string | Tag provided on entry via the `enter_tag` column in the dataframe. |
`is_short` | boolean | True for short trades, False otherwise | `is_short` | boolean | True for short trades, False otherwise. |
`orders` | Order[] | List of order objects attached to this trade (includes both filled and cancelled orders) | `orders` | Order[] | List of order objects attached to this trade (includes both filled and cancelled orders). |
`date_last_filled_utc` | datetime | Time of the last filled order | `date_last_filled_utc` | datetime | Time of the last filled order. |
`entry_side` | "buy" / "sell" | Order Side the trade was entered | `entry_side` | "buy" / "sell" | Order Side the trade was entered. |
`exit_side` | "buy" / "sell" | Order Side that will result in a trade exit / position reduction. | `exit_side` | "buy" / "sell" | Order Side that will result in a trade exit / position reduction. |
`trade_direction` | "long" / "short" | Trade direction in text - long or short. | `trade_direction` | "long" / "short" | Trade direction in text - long or short. |
`nr_of_successful_entries` | int | Number of successful (filled) entry orders | `nr_of_successful_entries` | int | Number of successful (filled) entry orders. |
`nr_of_successful_exits` | int | Number of successful (filled) exit orders | `nr_of_successful_exits` | int | Number of successful (filled) exit orders. |
## Class methods ## Class methods
+25 -7
View File
@@ -5,6 +5,30 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
Otherwise, please follow the instructions below. Otherwise, please follow the instructions below.
All instructions assume that python 3.9+ is installed and available.
## Clone the git repository
First of all clone the repository by running:
``` powershell
git clone https://github.com/freqtrade/freqtrade.git
```
Now, choose your installation method, either automatically via script (recommended) or manually following the corresponding instructions.
## Install freqtrade automatically
### Run the installation script
The script will ask you a few questions to determine which parts should be installed.
```powershell
Set-ExecutionPolicy -ExecutionPolicy Bypass
cd freqtrade
. .\setup.ps1
```
## Install freqtrade manually ## Install freqtrade manually
!!! Note "64bit Python version" !!! Note "64bit Python version"
@@ -14,13 +38,7 @@ Otherwise, please follow the instructions below.
!!! Hint !!! Hint
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information. Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
### 1. Clone the git repository ### Install ta-lib
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
### 2. Install ta-lib
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows). Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
+2 -2
View File
@@ -1,6 +1,6 @@
"""Freqtrade bot""" """Freqtrade bot"""
__version__ = "2024.5" __version__ = "2024.6"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
@@ -30,5 +30,5 @@ if "dev" in __version__:
versionfile = Path("./freqtrade_commit") versionfile = Path("./freqtrade_commit")
if versionfile.is_file(): if versionfile.is_file():
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}" __version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
except Exception: except Exception: # noqa: S110
pass pass
+1 -1
View File
@@ -187,7 +187,7 @@ def ask_user_config() -> Dict[str, Any]:
"Insert Api server Listen Address (0.0.0.0 for docker, " "Insert Api server Listen Address (0.0.0.0 for docker, "
"otherwise best left untouched)" "otherwise best left untouched)"
), ),
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", "default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", # noqa: S104
"when": lambda x: x["api_server"], "when": lambda x: x["api_server"],
}, },
{ {
+2 -1
View File
@@ -2,6 +2,7 @@
This module contains the configuration class This module contains the configuration class
""" """
import ast
import logging import logging
import warnings import warnings
from copy import deepcopy from copy import deepcopy
@@ -301,7 +302,7 @@ class Configuration:
# Edge section: # Edge section:
if "stoploss_range" in self.args and self.args["stoploss_range"]: if "stoploss_range" in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args["stoploss_range"]) txt_range = ast.literal_eval(self.args["stoploss_range"])
config["edge"].update({"stoploss_range_min": txt_range[0]}) config["edge"].update({"stoploss_range_min": txt_range[0]})
config["edge"].update({"stoploss_range_max": txt_range[1]}) config["edge"].update({"stoploss_range_max": txt_range[1]})
config["edge"].update({"stoploss_range_step": txt_range[2]}) config["edge"].update({"stoploss_range_step": txt_range[2]})
+5
View File
@@ -618,6 +618,11 @@ def download_data_main(config: Config) -> None:
# Start downloading # Start downloading
try: try:
if config.get("download_trades"): if config.get("download_trades"):
if not exchange.get_option("trades_has_history", True):
raise OperationalException(
f"Trade history not available for {exchange.name}. "
"You cannot use --dl-trades for this exchange."
)
pairs_not_available = refresh_backtest_trades_data( pairs_not_available = refresh_backtest_trades_data(
exchange, exchange,
pairs=expanded_pairs, pairs=expanded_pairs,
+1 -1
View File
@@ -1,7 +1,7 @@
from enum import Enum from enum import Enum
class RunMode(Enum): class RunMode(str, Enum):
""" """
Bot running mode (backtest, hyperopt, ...) Bot running mode (backtest, hyperopt, ...)
can be "live", "dry-run", "backtest", "edge", "hyperopt". can be "live", "dry-run", "backtest", "edge", "hyperopt".
+1
View File
@@ -28,6 +28,7 @@ class Binance(Exchange):
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "fromId", "trades_pagination_arg": "fromId",
"trades_has_history": True,
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
File diff suppressed because it is too large Load Diff
+1
View File
@@ -20,4 +20,5 @@ class Bingx(Exchange):
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"}, "stoploss_order_types": {"limit": "limit", "market": "market"},
"order_time_in_force": ["GTC", "IOC", "PO"], "order_time_in_force": ["GTC", "IOC", "PO"],
"trades_has_history": False, # Endpoint doesn't seem to support pagination
} }
+1
View File
@@ -18,4 +18,5 @@ class Bitmart(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": False, # Bitmart API does not support stoploss orders "stoploss_on_exchange": False, # Bitmart API does not support stoploss orders
"ohlcv_candle_limit": 200, "ohlcv_candle_limit": 200,
"trades_has_history": False, # Endpoint doesn't seem to support pagination
} }
+1
View File
@@ -33,6 +33,7 @@ class Bybit(Exchange):
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"ohlcv_has_history": True, "ohlcv_has_history": True,
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"], "order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
"trades_has_history": False, # Endpoint doesn't support pagination
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"ohlcv_has_history": True, "ohlcv_has_history": True,
+31 -29
View File
@@ -117,6 +117,7 @@ class Exchange:
"tickers_have_price": True, "tickers_have_price": True,
"trades_pagination": "time", # Possible are "time" or "id" "trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
"trades_has_history": False,
"l2_limit_range": None, "l2_limit_range": None,
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"mark_ohlcv_price": "mark", "mark_ohlcv_price": "mark",
@@ -151,7 +152,7 @@ class Exchange:
:return: None :return: None
""" """
self._api: ccxt.Exchange self._api: ccxt.Exchange
self._api_async: ccxt_async.Exchange = None self._api_async: ccxt_async.Exchange
self._markets: Dict = {} self._markets: Dict = {}
self._trading_fees: Dict[str, Any] = {} self._trading_fees: Dict[str, Any] = {}
self._leverage_tiers: Dict[str, List[Dict]] = {} self._leverage_tiers: Dict[str, List[Dict]] = {}
@@ -233,7 +234,7 @@ class Exchange:
self.required_candle_call_count = 1 self.required_candle_call_count = 1
if validate: if validate:
# Initial markets load # Initial markets load
self._load_markets() self.reload_markets(True, load_leverage_tiers=False)
self.validate_config(config) self.validate_config(config)
self._startup_candle_count: int = config.get("startup_candle_count", 0) self._startup_candle_count: int = config.get("startup_candle_count", 0)
self.required_candle_call_count = self.validate_required_startup_candles( self.required_candle_call_count = self.validate_required_startup_candles(
@@ -258,7 +259,7 @@ class Exchange:
def close(self): def close(self):
logger.debug("Exchange object destroyed, closing async loop") logger.debug("Exchange object destroyed, closing async loop")
if ( if (
self._api_async getattr(self, "_api_async", None)
and inspect.iscoroutinefunction(self._api_async.close) and inspect.iscoroutinefunction(self._api_async.close)
and self._api_async.session and self._api_async.session
): ):
@@ -354,7 +355,7 @@ class Exchange:
"""exchange ccxt markets""" """exchange ccxt markets"""
if not self._markets: if not self._markets:
logger.info("Markets were not loaded. Loading them now..") logger.info("Markets were not loaded. Loading them now..")
self._load_markets() self.reload_markets(True)
return self._markets return self._markets
@property @property
@@ -530,30 +531,26 @@ class Exchange:
amount, self.get_precision_amount(pair), self.precisionMode, contract_size amount, self.get_precision_amount(pair), self.precisionMode, contract_size
) )
def _load_async_markets(self, reload: bool = False) -> None: def _load_async_markets(self, reload: bool = False) -> Dict[str, Any]:
try: try:
if self._api_async: markets = self.loop.run_until_complete(
self.loop.run_until_complete(self._api_async.load_markets(reload=reload, params={})) self._api_async.load_markets(reload=reload, params={})
)
except (asyncio.TimeoutError, ccxt.BaseError) as e: if isinstance(markets, Exception):
logger.warning("Could not load async markets. Reason: %s", e) raise markets
return return markets
except asyncio.TimeoutError as e:
logger.warning("Could not load markets. Reason: %s", e)
raise TemporaryError from e
def _load_markets(self) -> None: def reload_markets(self, force: bool = False, *, load_leverage_tiers: bool = True) -> None:
"""Initialize markets both sync and async""" """
try: Reload / Initialize markets both sync and async if refresh interval has passed
self._markets = self._api.load_markets(params={})
self._load_async_markets()
self._last_markets_refresh = dt_ts()
if self._ft_has["needs_trading_fees"]:
self._trading_fees = self.fetch_trading_fees()
except ccxt.BaseError: """
logger.exception("Unable to initialize markets.")
def reload_markets(self, force: bool = False) -> None:
"""Reload markets both sync and async if refresh interval has passed"""
# Check whether markets have to be reloaded # Check whether markets have to be reloaded
is_initial = self._last_markets_refresh == 0
if ( if (
not force not force
and self._last_markets_refresh > 0 and self._last_markets_refresh > 0
@@ -562,13 +559,18 @@ class Exchange:
return None return None
logger.debug("Performing scheduled market reload..") logger.debug("Performing scheduled market reload..")
try: try:
self._markets = self._api.load_markets(reload=True, params={}) # Reload async markets, then assign them to sync api
# Also reload async markets to avoid issues with newly listed pairs self._markets = self._load_async_markets(reload=True)
self._load_async_markets(reload=True) self._api.set_markets(self._api_async.markets, self._api_async.currencies)
self._last_markets_refresh = dt_ts() self._last_markets_refresh = dt_ts()
self.fill_leverage_tiers()
except ccxt.BaseError: if is_initial and self._ft_has["needs_trading_fees"]:
logger.exception("Could not reload markets.") self._trading_fees = self.fetch_trading_fees()
if load_leverage_tiers and self.trading_mode == TradingMode.FUTURES:
self.fill_leverage_tiers()
except (ccxt.BaseError, TemporaryError):
logger.exception("Could not load markets.")
def validate_stakecurrency(self, stake_currency: str) -> None: def validate_stakecurrency(self, stake_currency: str) -> None:
""" """
+1
View File
@@ -31,6 +31,7 @@ class Gate(Exchange):
"stop_price_param": "stopPrice", "stop_price_param": "stopPrice",
"stop_price_prop": "stopPrice", "stop_price_prop": "stopPrice",
"marketOrderRequiresPrice": True, "marketOrderRequiresPrice": True,
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
+1
View File
@@ -28,6 +28,7 @@ class Htx(Exchange):
"1w": 500, "1w": 500,
"1M": 500, "1M": 500,
}, },
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
} }
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
+1
View File
@@ -31,6 +31,7 @@ class Kraken(Exchange):
"trades_pagination": "id", "trades_pagination": "id",
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
"trades_pagination_overlap": False, "trades_pagination_overlap": False,
"trades_has_history": True,
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",
} }
+1
View File
@@ -33,6 +33,7 @@ class Okx(Exchange):
"funding_fee_timeframe": "8h", "funding_fee_timeframe": "8h",
"stoploss_order_types": {"limit": "limit"}, "stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"tickers_have_quoteVolume": False, "tickers_have_quoteVolume": False,
@@ -148,7 +148,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
the motivation here is that `n_steps` is easier to optimize and keep stable, the motivation here is that `n_steps` is easier to optimize and keep stable,
across different n_obs - the number of data points. across different n_obs - the number of data points.
""" """
assert isinstance(self.n_steps, int), "Either `n_steps` or `n_epochs` should be set." if not isinstance(self.n_steps, int):
raise ValueError("Either `n_steps` or `n_epochs` should be set.")
n_batches = n_obs // self.batch_size n_batches = n_obs // self.batch_size
n_epochs = max(self.n_steps // n_batches, 1) n_epochs = max(self.n_steps // n_batches, 1)
if n_epochs <= 10: if n_epochs <= 10:
+1 -1
View File
@@ -217,7 +217,7 @@ class FreqtradeBot(LoggingMixin):
except Exception: except Exception:
# Exceptions here will be happening if the db disappeared. # Exceptions here will be happening if the db disappeared.
# At which point we can no longer commit anyway. # At which point we can no longer commit anyway.
pass logger.exception("Error during cleanup")
def startup(self) -> None: def startup(self) -> None:
""" """
+1 -1
View File
@@ -13,7 +13,7 @@ def get_strategy_run_id(strategy) -> str:
:param strategy: strategy object. :param strategy: strategy object.
:return: hex string id. :return: hex string id.
""" """
digest = hashlib.sha1() digest = hashlib.sha1() # noqa: S324
config = deepcopy(strategy.config) config = deepcopy(strategy.config)
# Options that have no impact on results of individual backtest. # Options that have no impact on results of individual backtest.
+1 -1
View File
@@ -489,7 +489,7 @@ class Hyperopt:
) )
def _set_random_state(self, random_state: Optional[int]) -> int: def _set_random_state(self, random_state: Optional[int]) -> int:
return random_state or random.randint(1, 2**16 - 1) return random_state or random.randint(1, 2**16 - 1) # noqa: S311
def advise_and_trim(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def advise_and_trim(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
preprocessed = self.backtesting.strategy.advise_all_indicators(data) preprocessed = self.backtesting.strategy.advise_all_indicators(data)
@@ -1,5 +1,5 @@
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List, Union
from tabulate import tabulate from tabulate import tabulate
@@ -20,13 +20,13 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
def _get_line_header( def _get_line_header(
first_column: str, stake_currency: str, direction: str = "Entries" first_column: Union[str, List[str]], stake_currency: str, direction: str = "Trades"
) -> List[str]: ) -> List[str]:
""" """
Generate header lines (goes in line with _generate_result_line()) Generate header lines (goes in line with _generate_result_line())
""" """
return [ return [
first_column, *([first_column] if isinstance(first_column, str) else first_column),
direction, direction,
"Avg Profit %", "Avg Profit %",
f"Tot Profit {stake_currency}", f"Tot Profit {stake_currency}",
@@ -54,7 +54,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
:return: pretty printed table with tabulate as string :return: pretty printed table with tabulate as string
""" """
headers = _get_line_header("Pair", stake_currency) headers = _get_line_header("Pair", stake_currency, "Trades")
floatfmt = _get_line_floatfmt(stake_currency) floatfmt = _get_line_floatfmt(stake_currency)
output = [ output = [
[ [
@@ -79,20 +79,30 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
:param stake_currency: stake-currency - used to correctly name headers :param stake_currency: stake-currency - used to correctly name headers
:return: pretty printed table with tabulate as string :return: pretty printed table with tabulate as string
""" """
floatfmt = _get_line_floatfmt(stake_currency)
fallback: str = "" fallback: str = ""
is_list = False
if tag_type == "enter_tag": if tag_type == "enter_tag":
headers = _get_line_header("TAG", stake_currency) headers = _get_line_header("Enter Tag", stake_currency, "Entries")
else: elif tag_type == "exit_tag":
headers = _get_line_header("Exit Reason", stake_currency, "Exits") headers = _get_line_header("Exit Reason", stake_currency, "Exits")
fallback = "exit_reason" fallback = "exit_reason"
else:
# Mix tag
headers = _get_line_header(["Enter Tag", "Exit Reason"], stake_currency, "Trades")
floatfmt.insert(0, "s")
is_list = True
floatfmt = _get_line_floatfmt(stake_currency)
output = [ output = [
[ [
( *(
t["key"] (
(t["key"] if isinstance(t["key"], list) else [t["key"], ""])
if is_list
else [t["key"]]
)
if t.get("key") is not None and len(str(t["key"])) > 0 if t.get("key") is not None and len(str(t["key"])) > 0
else t.get(fallback, "OTHER") else [t.get(fallback, "OTHER")]
), ),
t["trades"], t["trades"],
t["profit_mean_pct"], t["profit_mean_pct"],
@@ -144,7 +154,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
:return: pretty printed table with tabulate as string :return: pretty printed table with tabulate as string
""" """
floatfmt = _get_line_floatfmt(stake_currency) floatfmt = _get_line_floatfmt(stake_currency)
headers = _get_line_header("Strategy", stake_currency) headers = _get_line_header("Strategy", stake_currency, "Trades")
# _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless # _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless
# therefore we slip this column in only for strategy summary here. # therefore we slip this column in only for strategy summary here.
headers.append("Drawdown") headers.append("Drawdown")
@@ -380,6 +390,32 @@ def text_table_add_metrics(strat_results: Dict) -> str:
return message return message
def _show_tag_subresults(results: Dict[str, Any], stake_currency: str):
"""
Print tag subresults (enter_tag, exit_reason_summary, mix_tag_stats)
"""
if (enter_tags := results.get("results_per_enter_tag")) is not None:
table = text_table_tags("enter_tag", enter_tags, stake_currency)
if isinstance(table, str) and len(table) > 0:
print(" ENTER TAG STATS ".center(len(table.splitlines()[0]), "="))
print(table)
if (exit_reasons := results.get("exit_reason_summary")) is not None:
table = text_table_tags("exit_tag", exit_reasons, stake_currency)
if isinstance(table, str) and len(table) > 0:
print(" EXIT REASON STATS ".center(len(table.splitlines()[0]), "="))
print(table)
if (mix_tag := results.get("mix_tag_stats")) is not None:
table = text_table_tags("mix_tag", mix_tag, stake_currency)
if isinstance(table, str) and len(table) > 0:
print(" MIXED TAG STATS ".center(len(table.splitlines()[0]), "="))
print(table)
def show_backtest_result( def show_backtest_result(
strategy: str, results: Dict[str, Any], stake_currency: str, backtest_breakdown: List[str] strategy: str, results: Dict[str, Any], stake_currency: str, backtest_breakdown: List[str]
): ):
@@ -398,19 +434,7 @@ def show_backtest_result(
print(" LEFT OPEN TRADES REPORT ".center(len(table.splitlines()[0]), "=")) print(" LEFT OPEN TRADES REPORT ".center(len(table.splitlines()[0]), "="))
print(table) print(table)
if (enter_tags := results.get("results_per_enter_tag")) is not None: _show_tag_subresults(results, stake_currency)
table = text_table_tags("enter_tag", enter_tags, stake_currency)
if isinstance(table, str) and len(table) > 0:
print(" ENTER TAG STATS ".center(len(table.splitlines()[0]), "="))
print(table)
if (exit_reasons := results.get("exit_reason_summary")) is not None:
table = text_table_tags("exit_tag", exit_reasons, stake_currency)
if isinstance(table, str) and len(table) > 0:
print(" EXIT REASON STATS ".center(len(table.splitlines()[0]), "="))
print(table)
for period in backtest_breakdown: for period in backtest_breakdown:
if period in results.get("periodic_breakdown", {}): if period in results.get("periodic_breakdown", {}):
@@ -1,7 +1,7 @@
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Tuple, Union from typing import Any, Dict, List, Literal, Tuple, Union
import numpy as np import numpy as np
from pandas import DataFrame, Series, concat, to_datetime from pandas import DataFrame, Series, concat, to_datetime
@@ -68,7 +68,9 @@ def generate_rejected_signals(
return rejected_candles_only return rejected_candles_only
def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: def _generate_result_line(
result: DataFrame, starting_balance: int, first_column: Union[str, List[str]]
) -> Dict:
""" """
Generate one result dict, with "first_column" as key. Generate one result dict, with "first_column" as key.
""" """
@@ -141,7 +143,10 @@ def generate_pair_metrics(
def generate_tag_metrics( def generate_tag_metrics(
tag_type: str, starting_balance: int, results: DataFrame, skip_nan: bool = False tag_type: Union[Literal["enter_tag", "exit_reason"], List[Literal["enter_tag", "exit_reason"]]],
starting_balance: int,
results: DataFrame,
skip_nan: bool = False,
) -> List[Dict]: ) -> List[Dict]:
""" """
Generates and returns a list of metrics for the given tag trades and the results dataframe Generates and returns a list of metrics for the given tag trades and the results dataframe
@@ -153,13 +158,14 @@ def generate_tag_metrics(
tabular_data = [] tabular_data = []
if tag_type in results.columns: if all(
for tag, count in results[tag_type].value_counts().items(): tag in results.columns for tag in (tag_type if isinstance(tag_type, list) else [tag_type])
result = results[results[tag_type] == tag] ):
if skip_nan and result["profit_abs"].isnull().all(): for tags, group in results.groupby(tag_type):
if skip_nan and group["profit_abs"].isnull().all():
continue continue
tabular_data.append(_generate_result_line(result, starting_balance, tag)) tabular_data.append(_generate_result_line(group, starting_balance, tags))
# Sort by total profit %: # Sort by total profit %:
tabular_data = sorted(tabular_data, key=lambda k: k["profit_total_abs"], reverse=True) tabular_data = sorted(tabular_data, key=lambda k: k["profit_total_abs"], reverse=True)
@@ -378,12 +384,18 @@ def generate_strategy_stats(
skip_nan=False, skip_nan=False,
) )
enter_tag_results = generate_tag_metrics( enter_tag_stats = generate_tag_metrics(
"enter_tag", starting_balance=start_balance, results=results, skip_nan=False "enter_tag", starting_balance=start_balance, results=results, skip_nan=False
) )
exit_reason_stats = generate_tag_metrics( exit_reason_stats = generate_tag_metrics(
"exit_reason", starting_balance=start_balance, results=results, skip_nan=False "exit_reason", starting_balance=start_balance, results=results, skip_nan=False
) )
mix_tag_stats = generate_tag_metrics(
["enter_tag", "exit_reason"],
starting_balance=start_balance,
results=results,
skip_nan=False,
)
left_open_results = generate_pair_metrics( left_open_results = generate_pair_metrics(
pairlist, pairlist,
stake_currency=stake_currency, stake_currency=stake_currency,
@@ -425,8 +437,9 @@ def generate_strategy_stats(
"best_pair": best_pair, "best_pair": best_pair,
"worst_pair": worst_pair, "worst_pair": worst_pair,
"results_per_pair": pair_results, "results_per_pair": pair_results,
"results_per_enter_tag": enter_tag_results, "results_per_enter_tag": enter_tag_stats,
"exit_reason_summary": exit_reason_stats, "exit_reason_summary": exit_reason_stats,
"mix_tag_stats": mix_tag_stats,
"left_open_trades": left_open_results, "left_open_trades": left_open_results,
"total_trades": len(results), "total_trades": len(results),
"trade_count_long": len(results.loc[~results["is_short"]]), "trade_count_long": len(results.loc[~results["is_short"]]),
+7 -14
View File
@@ -5,11 +5,11 @@ Minimum age (days listed) pair list filter
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, List, Optional from typing import Dict, List, Optional
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import Config, ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.misc import plural from freqtrade.misc import plural
@@ -21,24 +21,17 @@ logger = logging.getLogger(__name__)
class AgeFilter(IPairList): class AgeFilter(IPairList):
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
# Checked symbols cache (dictionary of ticker symbol => timestamp) # Checked symbols cache (dictionary of ticker symbol => timestamp)
self._symbolsChecked: Dict[str, int] = {} self._symbolsChecked: Dict[str, int] = {}
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400) self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
self._min_days_listed = pairlistconfig.get("min_days_listed", 10) self._min_days_listed = self._pairlistconfig.get("min_days_listed", 10)
self._max_days_listed = pairlistconfig.get("max_days_listed") self._max_days_listed = self._pairlistconfig.get("max_days_listed")
candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"]) candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
if self._min_days_listed < 1: if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed to be >= 1") raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
if self._min_days_listed > candle_limit: if self._min_days_listed > candle_limit:
+1 -12
View File
@@ -3,9 +3,8 @@ Full trade slots pair list filter
""" """
import logging import logging
from typing import Any, Dict, List from typing import List
from freqtrade.constants import Config
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.plugins.pairlist.IPairList import IPairList
@@ -15,16 +14,6 @@ logger = logging.getLogger(__name__)
class FullTradesFilter(IPairList): class FullTradesFilter(IPairList):
def __init__(
self,
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
""" """
+3 -2
View File
@@ -3,7 +3,7 @@ PairList Handler base class
""" """
import logging import logging
from abc import ABC, abstractmethod, abstractproperty from abc import ABC, abstractmethod
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
@@ -87,7 +87,8 @@ class IPairList(LoggingMixin, ABC):
""" """
return self.__class__.__name__ return self.__class__.__name__
@abstractproperty @property
@abstractmethod
def needstickers(self) -> bool: def needstickers(self) -> bool:
""" """
Boolean property defining if tickers are necessary. Boolean property defining if tickers are necessary.
@@ -5,11 +5,10 @@ Provides dynamic pair list based on Market Cap
""" """
import logging import logging
from typing import Any, Dict, List from typing import Dict, List
from cachetools import TTLCache from cachetools import TTLCache
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
@@ -22,15 +21,8 @@ logger = logging.getLogger(__name__)
class MarketCapPairList(IPairList): class MarketCapPairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if "number_assets" not in self._pairlistconfig: if "number_assets" not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
@@ -38,14 +30,14 @@ class MarketCapPairList(IPairList):
'for "pairlist.config.number_assets"' 'for "pairlist.config.number_assets"'
) )
self._stake_currency = config["stake_currency"] self._stake_currency = self._config["stake_currency"]
self._number_assets = self._pairlistconfig["number_assets"] self._number_assets = self._pairlistconfig["number_assets"]
self._max_rank = self._pairlistconfig.get("max_rank", 30) self._max_rank = self._pairlistconfig.get("max_rank", 30)
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400) self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period) self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._def_candletype = self._config["candle_type_def"] self._def_candletype = self._config["candle_type_def"]
_coingecko_config = config.get("coingecko", {}) _coingecko_config = self._config.get("coingecko", {})
self._coingecko: FtCoinGeckoApi = FtCoinGeckoApi( self._coingecko: FtCoinGeckoApi = FtCoinGeckoApi(
api_key=_coingecko_config.get("api_key", ""), api_key=_coingecko_config.get("api_key", ""),
+5 -13
View File
@@ -3,9 +3,8 @@ Offset pair list filter
""" """
import logging import logging
from typing import Any, Dict, List from typing import Dict, List
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
@@ -15,18 +14,11 @@ logger = logging.getLogger(__name__)
class OffsetFilter(IPairList): class OffsetFilter(IPairList):
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._offset = pairlistconfig.get("offset", 0) self._offset = self._pairlistconfig.get("offset", 0)
self._number_pairs = pairlistconfig.get("number_assets", 0) self._number_pairs = self._pairlistconfig.get("number_assets", 0)
if self._offset < 0: if self._offset < 0:
raise OperationalException("OffsetFilter requires offset to be >= 0") raise OperationalException("OffsetFilter requires offset to be >= 0")
@@ -3,11 +3,10 @@ Performance pair list filter
""" """
import logging import logging
from typing import Any, Dict, List from typing import Dict, List
import pandas as pd import pandas as pd
from freqtrade.constants import Config
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
@@ -17,18 +16,11 @@ logger = logging.getLogger(__name__)
class PerformanceFilter(IPairList): class PerformanceFilter(IPairList):
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._minutes = pairlistconfig.get("minutes", 0) self._minutes = self._pairlistconfig.get("minutes", 0)
self._min_profit = pairlistconfig.get("min_profit") self._min_profit = self._pairlistconfig.get("min_profit")
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
+3 -11
View File
@@ -3,9 +3,8 @@ Precision pair list filter
""" """
import logging import logging
from typing import Any, Dict, Optional from typing import Optional
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import ROUND_UP from freqtrade.exchange import ROUND_UP
from freqtrade.exchange.types import Ticker from freqtrade.exchange.types import Ticker
@@ -16,15 +15,8 @@ logger = logging.getLogger(__name__)
class PrecisionFilter(IPairList): class PrecisionFilter(IPairList):
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if "stoploss" not in self._config: if "stoploss" not in self._config:
raise OperationalException( raise OperationalException(
+7 -15
View File
@@ -3,9 +3,8 @@ Price pair list filter
""" """
import logging import logging
from typing import Any, Dict, Optional from typing import Dict, Optional
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Ticker from freqtrade.exchange.types import Ticker
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
@@ -15,26 +14,19 @@ logger = logging.getLogger(__name__)
class PriceFilter(IPairList): class PriceFilter(IPairList):
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._low_price_ratio = pairlistconfig.get("low_price_ratio", 0) self._low_price_ratio = self._pairlistconfig.get("low_price_ratio", 0)
if self._low_price_ratio < 0: if self._low_price_ratio < 0:
raise OperationalException("PriceFilter requires low_price_ratio to be >= 0") raise OperationalException("PriceFilter requires low_price_ratio to be >= 0")
self._min_price = pairlistconfig.get("min_price", 0) self._min_price = self._pairlistconfig.get("min_price", 0)
if self._min_price < 0: if self._min_price < 0:
raise OperationalException("PriceFilter requires min_price to be >= 0") raise OperationalException("PriceFilter requires min_price to be >= 0")
self._max_price = pairlistconfig.get("max_price", 0) self._max_price = self._pairlistconfig.get("max_price", 0)
if self._max_price < 0: if self._max_price < 0:
raise OperationalException("PriceFilter requires max_price to be >= 0") raise OperationalException("PriceFilter requires max_price to be >= 0")
self._max_value = pairlistconfig.get("max_value", 0) self._max_value = self._pairlistconfig.get("max_value", 0)
if self._max_value < 0: if self._max_value < 0:
raise OperationalException("PriceFilter requires max_value to be >= 0") raise OperationalException("PriceFilter requires max_value to be >= 0")
self._enabled = ( self._enabled = (
+4 -11
View File
@@ -5,7 +5,7 @@ Provides pair list from Leader data
""" """
import logging import logging
from typing import Any, Dict, List, Optional from typing import Dict, List, Optional
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
@@ -32,19 +32,12 @@ class ProducerPairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Dict[str, Any],
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._num_assets: int = self._pairlistconfig.get("number_assets", 0) self._num_assets: int = self._pairlistconfig.get("number_assets", 0)
self._producer_name = self._pairlistconfig.get("producer_name", "default") self._producer_name = self._pairlistconfig.get("producer_name", "default")
if not config.get("external_message_consumer", {}).get("enabled"): if not self._config.get("external_message_consumer", {}).get("enabled"):
raise OperationalException( raise OperationalException(
"ProducerPairList requires external_message_consumer to be enabled." "ProducerPairList requires external_message_consumer to be enabled."
) )
+2 -10
View File
@@ -14,7 +14,6 @@ from cachetools import TTLCache
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.configuration.load_config import CONFIG_PARSE_MODE from freqtrade.configuration.load_config import CONFIG_PARSE_MODE
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
@@ -27,15 +26,8 @@ logger = logging.getLogger(__name__)
class RemotePairList(IPairList): class RemotePairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if "number_assets" not in self._pairlistconfig: if "number_assets" not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
+7 -15
View File
@@ -4,9 +4,8 @@ Shuffle pair list filter
import logging import logging
import random import random
from typing import Any, Dict, List, Literal from typing import Dict, List, Literal
from freqtrade.constants import Config
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
@@ -20,27 +19,20 @@ ShuffleValues = Literal["candle", "iteration"]
class ShuffleFilter(IPairList): class ShuffleFilter(IPairList):
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
# Apply seed in backtesting mode to get comparable results, # Apply seed in backtesting mode to get comparable results,
# but not in live modes to get a non-repeating order of pairs during live modes. # but not in live modes to get a non-repeating order of pairs during live modes.
if config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN): if self._config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN):
self._seed = None self._seed = None
logger.info("Live mode detected, not applying seed.") logger.info("Live mode detected, not applying seed.")
else: else:
self._seed = pairlistconfig.get("seed") self._seed = self._pairlistconfig.get("seed")
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}") logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
self._random = random.Random(self._seed) self._random = random.Random(self._seed) # noqa: S311
self._shuffle_freq: ShuffleValues = pairlistconfig.get("shuffle_frequency", "candle") self._shuffle_freq: ShuffleValues = self._pairlistconfig.get("shuffle_frequency", "candle")
self.__pairlist_cache = PeriodicCache( self.__pairlist_cache = PeriodicCache(
maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"]) maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"])
) )
+4 -12
View File
@@ -3,9 +3,8 @@ Spread pair list filter
""" """
import logging import logging
from typing import Any, Dict, Optional from typing import Dict, Optional
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Ticker from freqtrade.exchange.types import Ticker
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
@@ -15,17 +14,10 @@ logger = logging.getLogger(__name__)
class SpreadFilter(IPairList): class SpreadFilter(IPairList):
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._max_spread_ratio = pairlistconfig.get("max_spread_ratio", 0.005) self._max_spread_ratio = self._pairlistconfig.get("max_spread_ratio", 0.005)
self._enabled = self._max_spread_ratio != 0 self._enabled = self._max_spread_ratio != 0
if not self._exchange.get_option("tickers_have_bid_ask"): if not self._exchange.get_option("tickers_have_bid_ask"):
+3 -11
View File
@@ -6,9 +6,8 @@ Provides pair white list as it configured in config
import logging import logging
from copy import deepcopy from copy import deepcopy
from typing import Any, Dict, List from typing import Dict, List
from freqtrade.constants import Config
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
@@ -19,15 +18,8 @@ logger = logging.getLogger(__name__)
class StaticPairList(IPairList): class StaticPairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._allow_inactive = self._pairlistconfig.get("allow_inactive", False) self._allow_inactive = self._pairlistconfig.get("allow_inactive", False)
+10 -17
View File
@@ -5,13 +5,13 @@ Volatility pairlist filter
import logging import logging
import sys import sys
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, List, Optional from typing import Dict, List, Optional
import numpy as np import numpy as np
from cachetools import TTLCache from cachetools import TTLCache
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import Config, ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.misc import plural from freqtrade.misc import plural
@@ -27,26 +27,19 @@ class VolatilityFilter(IPairList):
Filters pairs by volatility Filters pairs by volatility
""" """
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._days = pairlistconfig.get("lookback_days", 10) self._days = self._pairlistconfig.get("lookback_days", 10)
self._min_volatility = pairlistconfig.get("min_volatility", 0) self._min_volatility = self._pairlistconfig.get("min_volatility", 0)
self._max_volatility = pairlistconfig.get("max_volatility", sys.maxsize) self._max_volatility = self._pairlistconfig.get("max_volatility", sys.maxsize)
self._refresh_period = pairlistconfig.get("refresh_period", 1440) self._refresh_period = self._pairlistconfig.get("refresh_period", 1440)
self._def_candletype = self._config["candle_type_def"] self._def_candletype = self._config["candle_type_def"]
self._sort_direction: Optional[str] = pairlistconfig.get("sort_direction", None) self._sort_direction: Optional[str] = self._pairlistconfig.get("sort_direction", None)
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"]) candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
if self._days < 1: if self._days < 1:
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1") raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
if self._days > candle_limit: if self._days > candle_limit:
+5 -12
View File
@@ -10,7 +10,7 @@ from typing import Any, Dict, List, Literal
from cachetools import TTLCache from cachetools import TTLCache
from freqtrade.constants import Config, ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
@@ -27,15 +27,8 @@ SORT_VALUES = ["quoteVolume"]
class VolumePairList(IPairList): class VolumePairList(IPairList):
is_pairlist_generator = True is_pairlist_generator = True
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if "number_assets" not in self._pairlistconfig: if "number_assets" not in self._pairlistconfig:
raise OperationalException( raise OperationalException(
@@ -43,7 +36,7 @@ class VolumePairList(IPairList):
'for "pairlist.config.number_assets"' 'for "pairlist.config.number_assets"'
) )
self._stake_currency = config["stake_currency"] self._stake_currency = self._config["stake_currency"]
self._number_pairs = self._pairlistconfig["number_assets"] self._number_pairs = self._pairlistconfig["number_assets"]
self._sort_key: Literal["quoteVolume"] = self._pairlistconfig.get("sort_key", "quoteVolume") self._sort_key: Literal["quoteVolume"] = self._pairlistconfig.get("sort_key", "quoteVolume")
self._min_value = self._pairlistconfig.get("min_value", 0) self._min_value = self._pairlistconfig.get("min_value", 0)
@@ -94,7 +87,7 @@ class VolumePairList(IPairList):
if not self._validate_keys(self._sort_key): if not self._validate_keys(self._sort_key):
raise OperationalException(f"key {self._sort_key} not in {SORT_VALUES}") raise OperationalException(f"key {self._sort_key} not in {SORT_VALUES}")
candle_limit = exchange.ohlcv_candle_limit( candle_limit = self._exchange.ohlcv_candle_limit(
self._lookback_timeframe, self._config["candle_type_def"] self._lookback_timeframe, self._config["candle_type_def"]
) )
if self._lookback_period < 0: if self._lookback_period < 0:
@@ -4,12 +4,12 @@ Rate of change pairlist filter
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Any, Dict, List, Optional from typing import Dict, List, Optional
from cachetools import TTLCache from cachetools import TTLCache
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import Config, ListPairsWithTimeframes from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers from freqtrade.exchange.types import Tickers
from freqtrade.misc import plural from freqtrade.misc import plural
@@ -21,26 +21,19 @@ logger = logging.getLogger(__name__)
class RangeStabilityFilter(IPairList): class RangeStabilityFilter(IPairList):
def __init__( def __init__(self, *args, **kwargs) -> None:
self, super().__init__(*args, **kwargs)
exchange,
pairlistmanager,
config: Config,
pairlistconfig: Dict[str, Any],
pairlist_pos: int,
) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._days = pairlistconfig.get("lookback_days", 10) self._days = self._pairlistconfig.get("lookback_days", 10)
self._min_rate_of_change = pairlistconfig.get("min_rate_of_change", 0.01) self._min_rate_of_change = self._pairlistconfig.get("min_rate_of_change", 0.01)
self._max_rate_of_change = pairlistconfig.get("max_rate_of_change") self._max_rate_of_change = self._pairlistconfig.get("max_rate_of_change")
self._refresh_period = pairlistconfig.get("refresh_period", 86400) self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
self._def_candletype = self._config["candle_type_def"] self._def_candletype = self._config["candle_type_def"]
self._sort_direction: Optional[str] = pairlistconfig.get("sort_direction", None) self._sort_direction: Optional[str] = self._pairlistconfig.get("sort_direction", None)
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"]) candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
if self._days < 1: if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
if self._days > candle_limit: if self._days > candle_limit:
+15 -7
View File
@@ -31,7 +31,7 @@ security = HTTPBasic()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
def get_user_from_token(token, secret_key: str, token_type: str = "access") -> str: def get_user_from_token(token, secret_key: str, token_type: str = "access") -> str: # noqa: S107
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials", detail="Could not validate credentials",
@@ -86,11 +86,11 @@ async def validate_ws_token(
await ws.close(code=status.WS_1008_POLICY_VIOLATION) await ws.close(code=status.WS_1008_POLICY_VIOLATION)
def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: # noqa: S107
to_encode = data.copy() to_encode = data.copy()
if token_type == "access": if token_type == "access": # noqa: S105
expire = datetime.now(timezone.utc) + timedelta(minutes=15) expire = datetime.now(timezone.utc) + timedelta(minutes=15)
elif token_type == "refresh": elif token_type == "refresh": # noqa: S105
expire = datetime.now(timezone.utc) + timedelta(days=30) expire = datetime.now(timezone.utc) + timedelta(days=30)
else: else:
raise ValueError() raise ValueError()
@@ -127,9 +127,15 @@ def token_login(
): ):
if verify_auth(api_config, form_data.username, form_data.password): if verify_auth(api_config, form_data.username, form_data.password):
token_data = {"identity": {"u": form_data.username}} token_data = {"identity": {"u": form_data.username}}
access_token = create_token(token_data, api_config.get("jwt_secret_key", "super-secret")) access_token = create_token(
token_data,
api_config.get("jwt_secret_key", "super-secret"),
token_type="access", # noqa: S106
)
refresh_token = create_token( refresh_token = create_token(
token_data, api_config.get("jwt_secret_key", "super-secret"), token_type="refresh" token_data,
api_config.get("jwt_secret_key", "super-secret"),
token_type="refresh", # noqa: S106
) )
return { return {
"access_token": access_token, "access_token": access_token,
@@ -148,6 +154,8 @@ def token_refresh(token: str = Depends(oauth2_scheme), api_config=Depends(get_ap
u = get_user_from_token(token, api_config.get("jwt_secret_key", "super-secret"), "refresh") u = get_user_from_token(token, api_config.get("jwt_secret_key", "super-secret"), "refresh")
token_data = {"identity": {"u": u}} token_data = {"identity": {"u": u}}
access_token = create_token( access_token = create_token(
token_data, api_config.get("jwt_secret_key", "super-secret"), token_type="access" token_data,
api_config.get("jwt_secret_key", "super-secret"),
token_type="access", # noqa: S106
) )
return {"access_token": access_token} return {"access_token": access_token}
+2
View File
@@ -1466,6 +1466,8 @@ class RPC:
from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
# Manually load hyperparameters, as we don't call the bot-start callback.
strategy.ft_load_hyper_params(False)
if strategy.plot_config and "subplots" not in strategy.plot_config: if strategy.plot_config and "subplots" not in strategy.plot_config:
strategy.plot_config["subplots"] = {} strategy.plot_config["subplots"] = {}
+2 -2
View File
@@ -1787,7 +1787,7 @@ class Telegram(RPCHandler):
"_Bot Control_\n" "_Bot Control_\n"
"------------\n" "------------\n"
"*/start:* `Starts the trader`\n" "*/start:* `Starts the trader`\n"
"*/stop:* Stops the trader\n" "*/stop:* `Stops the trader`\n"
"*/stopentry:* `Stops entering, but handles open trades gracefully` \n" "*/stopentry:* `Stops entering, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, " "*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n" "regardless of profit`\n"
@@ -1820,7 +1820,7 @@ class Telegram(RPCHandler):
"that represents the current market direction. If no direction is provided `" "that represents the current market direction. If no direction is provided `"
"`the currently set market direction will be output.` \n" "`the currently set market direction will be output.` \n"
"*/list_custom_data <trade_id> <key>:* `List custom_data for Trade ID & Key combo.`\n" "*/list_custom_data <trade_id> <key>:* `List custom_data for Trade ID & Key combo.`\n"
"`If no Key is supplied it will list all key-value pairs found for that Trade ID.`" "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`\n"
"_Statistics_\n" "_Statistics_\n"
"------------\n" "------------\n"
"*/status <trade_id>|[table]:* `Lists all open trades`\n" "*/status <trade_id>|[table]:* `Lists all open trades`\n"
+4 -1
View File
@@ -6,6 +6,7 @@ This module defines the interface to apply for strategies
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import isinf, isnan
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
from pandas import DataFrame from pandas import DataFrame
@@ -1423,7 +1424,9 @@ class IStrategy(ABC, HyperStrategyMixin):
after_fill=after_fill, after_fill=after_fill,
) )
# Sanity check - error cases will return None # Sanity check - error cases will return None
if stop_loss_value_custom: if stop_loss_value_custom and not (
isnan(stop_loss_value_custom) or isinf(stop_loss_value_custom)
):
stop_loss_value = stop_loss_value_custom stop_loss_value = stop_loss_value_custom
trade.adjust_stop_loss( trade.adjust_stop_loss(
bound or current_rate, stop_loss_value, allow_refresh=after_fill bound or current_rate, stop_loss_value, allow_refresh=after_fill
+2 -2
View File
@@ -1,7 +1,7 @@
from freqtrade_client.ft_rest_client import FtRestClient from freqtrade_client.ft_rest_client import FtRestClient
__version__ = "2024.5" __version__ = "2024.6"
if "dev" in __version__: if "dev" in __version__:
from pathlib import Path from pathlib import Path
@@ -31,7 +31,7 @@ if "dev" in __version__:
versionfile = Path("./freqtrade_commit") versionfile = Path("./freqtrade_commit")
if versionfile.is_file(): if versionfile.is_file():
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}" __version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
except Exception: except Exception: # noqa: S110
pass pass
__all__ = ["FtRestClient"] __all__ = ["FtRestClient"]
+16 -5
View File
@@ -81,12 +81,12 @@ def print_commands():
print(f"{x}\n\t{doc}\n") print(f"{x}\n\t{doc}\n")
def main_exec(args: Dict[str, Any]): def main_exec(parsed: Dict[str, Any]):
if args.get("show"): if parsed.get("show"):
print_commands() print_commands()
sys.exit() sys.exit()
config = load_config(args["config"]) config = load_config(parsed["config"])
url = config.get("api_server", {}).get("listen_ip_address", "127.0.0.1") url = config.get("api_server", {}).get("listen_ip_address", "127.0.0.1")
port = config.get("api_server", {}).get("listen_port", "8080") port = config.get("api_server", {}).get("listen_port", "8080")
username = config.get("api_server", {}).get("username") username = config.get("api_server", {}).get("username")
@@ -96,13 +96,24 @@ def main_exec(args: Dict[str, Any]):
client = FtRestClient(server_url, username, password) client = FtRestClient(server_url, username, password)
m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")] m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")]
command = args["command"] command = parsed["command"]
if command not in m: if command not in m:
logger.error(f"Command {command} not defined") logger.error(f"Command {command} not defined")
print_commands() print_commands()
return return
print(json.dumps(getattr(client, command)(*args["command_arguments"]))) # Split arguments with = into key/value pairs
kwargs = {x.split("=")[0]: x.split("=")[1] for x in parsed["command_arguments"] if "=" in x}
args = [x for x in parsed["command_arguments"] if "=" not in x]
try:
res = getattr(client, command)(*args, **kwargs)
print(json.dumps(res))
except TypeError as e:
logger.error(f"Error executing command {command}: {e}")
sys.exit(1)
except Exception as e:
logger.error(f"Fatal Error executing command {command}: {e}")
sys.exit(1)
def main(): def main():
+30 -2
View File
@@ -54,7 +54,7 @@ class FtRestClient:
# return resp.text # return resp.text
return resp.json() return resp.json()
except ConnectionError: except ConnectionError:
logger.warning("Connection error") logger.warning(f"Connection error - could not connect to {netloc}.")
def _get(self, apipath, params: ParamsT = None): def _get(self, apipath, params: ParamsT = None):
return self._call("GET", apipath, params=params) return self._call("GET", apipath, params=params)
@@ -312,20 +312,48 @@ class FtRestClient:
data = {"pair": pair, "price": price} data = {"pair": pair, "price": price}
return self._post("forcebuy", data=data) return self._post("forcebuy", data=data)
def forceenter(self, pair, side, price=None): def forceenter(
self,
pair,
side,
price=None,
*,
order_type=None,
stake_amount=None,
leverage=None,
enter_tag=None,
):
"""Force entering a trade """Force entering a trade
:param pair: Pair to buy (ETH/BTC) :param pair: Pair to buy (ETH/BTC)
:param side: 'long' or 'short' :param side: 'long' or 'short'
:param price: Optional - price to buy :param price: Optional - price to buy
:param order_type: Optional keyword argument - 'limit' or 'market'
:param stake_amount: Optional keyword argument - stake amount (as float)
:param leverage: Optional keyword argument - leverage (as float)
:param enter_tag: Optional keyword argument - entry tag (as string, default: 'force_enter')
:return: json object of the trade :return: json object of the trade
""" """
data = { data = {
"pair": pair, "pair": pair,
"side": side, "side": side,
} }
if price: if price:
data["price"] = price data["price"] = price
if order_type:
data["ordertype"] = order_type
if stake_amount:
data["stakeamount"] = stake_amount
if leverage:
data["leverage"] = leverage
if enter_tag:
data["entry_tag"] = enter_tag
return self._post("forceenter", data=data) return self._post("forceenter", data=data)
def forceexit(self, tradeid, ordertype=None, amount=None): def forceexit(self, tradeid, ordertype=None, amount=None):
+1 -1
View File
@@ -1,3 +1,3 @@
# Requirements for freqtrade client library # Requirements for freqtrade client library
requests==2.32.2 requests==2.32.3
python-rapidjson==1.17 python-rapidjson==1.17
+116 -60
View File
@@ -1,5 +1,5 @@
import re import re
from unittest.mock import MagicMock from unittest.mock import ANY, MagicMock
import pytest import pytest
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
@@ -52,70 +52,89 @@ def test_FtRestClient_call_invalid(caplog):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"method,args", "method,args,kwargs",
[ [
("start", []), ("start", [], {}),
("stop", []), ("stop", [], {}),
("stopbuy", []), ("stopbuy", [], {}),
("reload_config", []), ("reload_config", [], {}),
("balance", []), ("balance", [], {}),
("count", []), ("count", [], {}),
("entries", []), ("entries", [], {}),
("exits", []), ("exits", [], {}),
("mix_tags", []), ("mix_tags", [], {}),
("locks", []), ("locks", [], {}),
("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"]), ("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"], {}),
("delete_lock", [2]), ("delete_lock", [2], {}),
("daily", []), ("daily", [], {}),
("daily", [15]), ("daily", [15], {}),
("weekly", []), ("weekly", [], {}),
("weekly", [15]), ("weekly", [15], {}),
("monthly", []), ("monthly", [], {}),
("monthly", [12]), ("monthly", [12], {}),
("edge", []), ("edge", [], {}),
("profit", []), ("profit", [], {}),
("stats", []), ("stats", [], {}),
("performance", []), ("performance", [], {}),
("status", []), ("status", [], {}),
("version", []), ("version", [], {}),
("show_config", []), ("show_config", [], {}),
("ping", []), ("ping", [], {}),
("logs", []), ("logs", [], {}),
("logs", [55]), ("logs", [55], {}),
("trades", []), ("trades", [], {}),
("trades", [5]), ("trades", [5], {}),
("trades", [5, 5]), # With offset ("trades", [5, 5], {}), # With offset
("trade", [1]), ("trade", [1], {}),
("delete_trade", [1]), ("delete_trade", [1], {}),
("cancel_open_order", [1]), ("cancel_open_order", [1], {}),
("whitelist", []), ("whitelist", [], {}),
("blacklist", []), ("blacklist", [], {}),
("blacklist", ["XRP/USDT"]), ("blacklist", ["XRP/USDT"], {}),
("blacklist", ["XRP/USDT", "BTC/USDT"]), ("blacklist", ["XRP/USDT", "BTC/USDT"], {}),
("forcebuy", ["XRP/USDT"]), ("forcebuy", ["XRP/USDT"], {}),
("forcebuy", ["XRP/USDT", 1.5]), ("forcebuy", ["XRP/USDT", 1.5], {}),
("forceenter", ["XRP/USDT", "short"]), ("forceenter", ["XRP/USDT", "short"], {}),
("forceenter", ["XRP/USDT", "short", 1.5]), ("forceenter", ["XRP/USDT", "short", 1.5], {}),
("forceexit", [1]), ("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market"}),
("forceexit", [1, "limit"]), ("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market", "stake_amount": 100}),
("forceexit", [1, "limit", 100]), (
("strategies", []), "forceenter",
("strategy", ["sampleStrategy"]), ["XRP/USDT", "short", 1.5],
("pairlists_available", []), {"order_type": "market", "stake_amount": 100, "leverage": 10.0},
("plot_config", []), ),
("available_pairs", []), (
("available_pairs", ["5m"]), "forceenter",
("pair_candles", ["XRP/USDT", "5m"]), ["XRP/USDT", "short", 1.5],
("pair_candles", ["XRP/USDT", "5m", 500]), {
("pair_history", ["XRP/USDT", "5m", "SampleStrategy"]), "order_type": "market",
("sysinfo", []), "stake_amount": 100,
("health", []), "leverage": 10.0,
"enter_tag": "test_force_enter",
},
),
("forceexit", [1], {}),
("forceexit", [1, "limit"], {}),
("forceexit", [1, "limit", 100], {}),
("strategies", [], {}),
("strategy", ["sampleStrategy"], {}),
("pairlists_available", [], {}),
("plot_config", [], {}),
("available_pairs", [], {}),
("available_pairs", ["5m"], {}),
("pair_candles", ["XRP/USDT", "5m"], {}),
("pair_candles", ["XRP/USDT", "5m", 500], {}),
("pair_candles", ["XRP/USDT", "5m", 500], {"columns": ["close_time,close"]}),
("pair_history", ["XRP/USDT", "5m", "SampleStrategy"], {}),
("pair_history", ["XRP/USDT", "5m"], {"strategy": "SampleStrategy"}),
("sysinfo", [], {}),
("health", [], {}),
], ],
) )
def test_FtRestClient_call_explicit_methods(method, args): def test_FtRestClient_call_explicit_methods(method, args, kwargs):
client, mock = get_rest_client() client, mock = get_rest_client()
exec = getattr(client, method) exec = getattr(client, method)
exec(*args) exec(*args, **kwargs)
assert mock.call_count == 1 assert mock.call_count == 1
@@ -148,3 +167,40 @@ def test_ft_client(mocker, capsys, caplog):
) )
main_exec(args) main_exec(args)
assert log_has_re("Command whatever not defined", caplog) assert log_has_re("Command whatever not defined", caplog)
@pytest.mark.parametrize(
"params, expected_args, expected_kwargs",
[
("forceenter BTC/USDT long", ["BTC/USDT", "long"], {}),
("forceenter BTC/USDT long limit", ["BTC/USDT", "long", "limit"], {}),
(
# Skip most parameters, only providing enter_tag
"forceenter BTC/USDT long enter_tag=deadBeef",
["BTC/USDT", "long"],
{"enter_tag": "deadBeef"},
),
(
"forceenter BTC/USDT long invalid_key=123",
[],
SystemExit,
# {"invalid_key": "deadBeef"},
),
],
)
def test_ft_client_argparsing(mocker, params, expected_args, expected_kwargs, caplog):
mocked_method = params.split(" ")[0]
mocker.patch("freqtrade_client.ft_client.load_config", return_value={}, autospec=True)
mm = mocker.patch(
f"freqtrade_client.ft_client.FtRestClient.{mocked_method}", return_value={}, autospec=True
)
args = add_arguments(params.split(" "))
if isinstance(expected_kwargs, dict):
main_exec(args)
mm.assert_called_once_with(ANY, *expected_args, **expected_kwargs)
else:
with pytest.raises(expected_kwargs):
main_exec(args)
assert log_has_re(f"Error executing command {mocked_method}: got an unexpected .*", caplog)
mm.assert_not_called()
+1
View File
@@ -1,5 +1,6 @@
site_name: Freqtrade site_name: Freqtrade
site_url: !ENV [READTHEDOCS_CANONICAL_URL, 'https://www.freqtrade.io/en/latest/'] site_url: !ENV [READTHEDOCS_CANONICAL_URL, 'https://www.freqtrade.io/en/latest/']
site_description: Freqtrade is a free and open source crypto trading bot written in Python, designed to support all major exchanges and be controlled via Telegram or builtin Web UI
repo_url: https://github.com/freqtrade/freqtrade repo_url: https://github.com/freqtrade/freqtrade
edit_uri: edit/develop/docs/ edit_uri: edit/develop/docs/
use_directory_urls: True use_directory_urls: True
+22 -2
View File
@@ -82,6 +82,9 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
known_first_party = ["freqtrade_client"] known_first_party = ["freqtrade_client"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
log_format = "%(asctime)s %(levelname)s %(message)s"
log_date_format = "%Y-%m-%d %H:%M:%S"
asyncio_mode = "auto" asyncio_mode = "auto"
addopts = "--dist loadscope" addopts = "--dist loadscope"
@@ -135,7 +138,7 @@ extend-select = [
# "EXE", # flake8-executable # "EXE", # flake8-executable
# "C4", # flake8-comprehensions # "C4", # flake8-comprehensions
"YTT", # flake8-2020 "YTT", # flake8-2020
# "S", # flake8-bandit "S", # flake8-bandit
# "DTZ", # flake8-datetimez # "DTZ", # flake8-datetimez
# "RSE", # flake8-raise # "RSE", # flake8-raise
# "TCH", # flake8-type-checking # "TCH", # flake8-type-checking
@@ -148,13 +151,30 @@ extend-ignore = [
"E272", # Multiple spaces before keyword "E272", # Multiple spaces before keyword
"E221", # Multiple spaces before operator "E221", # Multiple spaces before operator
"B007", # Loop control variable not used "B007", # Loop control variable not used
"S603", # `subprocess` call: check for execution of untrusted input
"S607", # Starting a process with a partial executable path
"S608", # Possible SQL injection vector through string-based query construction
] ]
[tool.ruff.lint.mccabe] [tool.ruff.lint.mccabe]
max-complexity = 12 max-complexity = 12
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"tests/*" = ["S"] "freqtrade/freqai/**/*.py" = [
"S311" # Standard pseudo-random generators are not suitable for cryptographic purposes
]
"tests/**/*.py" = [
"S101", # allow assert in tests
"S104", # Possible binding to all interfaces
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
"S105", # Possible hardcoded password assigned to: "secret"
"S106", # Possible hardcoded password assigned to argument: "token_type"
"S110", # `try`-`except`-`pass` detected, consider logging the exception
]
"ft_client/test_client/**/*.py" = [
"S101", # allow assert in tests
]
[tool.ruff.lint.flake8-bugbear] [tool.ruff.lint.flake8-bugbear]
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`. # Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
+3 -3
View File
@@ -7,10 +7,10 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==4.0.1 coveralls==4.0.1
ruff==0.4.5 ruff==0.4.10
mypy==1.10.0 mypy==1.10.0
pre-commit==3.7.1 pre-commit==3.7.1
pytest==8.2.1 pytest==8.2.2
pytest-asyncio==0.23.7 pytest-asyncio==0.23.7
pytest-cov==5.0.0 pytest-cov==5.0.0
pytest-mock==3.14.0 pytest-mock==3.14.0
@@ -26,6 +26,6 @@ nbconvert==7.16.4
# mypy types # mypy types
types-cachetools==5.3.0.7 types-cachetools==5.3.0.7
types-filelock==3.2.7 types-filelock==3.2.7
types-requests==2.32.0.20240523 types-requests==2.32.0.20240622
types-tabulate==0.9.0.20240106 types-tabulate==0.9.0.20240106
types-python-dateutil==2.9.0.20240316 types-python-dateutil==2.9.0.20240316
+2 -1
View File
@@ -2,7 +2,8 @@
-r requirements-freqai.txt -r requirements-freqai.txt
# Required for freqai-rl # Required for freqai-rl
torch==2.2.2 torch==2.3.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'
gymnasium==0.29.1 gymnasium==0.29.1
stable_baselines3==2.3.2 stable_baselines3==2.3.2
sb3_contrib>=2.2.1 sb3_contrib>=2.2.1
+2 -2
View File
@@ -6,7 +6,7 @@
scikit-learn==1.5.0 scikit-learn==1.5.0
joblib==1.4.2 joblib==1.4.2
catboost==1.2.5; 'arm' not in platform_machine catboost==1.2.5; 'arm' not in platform_machine
lightgbm==4.3.0 lightgbm==4.4.0
xgboost==2.0.3 xgboost==2.0.3
tensorboard==2.16.2 tensorboard==2.17.0
datasieve==0.1.7 datasieve==0.1.7
+1 -1
View File
@@ -5,4 +5,4 @@
scipy==1.13.1 scipy==1.13.1
scikit-learn==1.5.0 scikit-learn==1.5.0
ft-scikit-optimize==0.9.2 ft-scikit-optimize==0.9.2
filelock==3.14.0 filelock==3.15.4
+14 -12
View File
@@ -1,20 +1,22 @@
numpy==1.26.4 numpy==1.26.4
pandas==2.2.2 pandas==2.2.2
bottleneck==1.4.0
numexpr==2.10.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==4.3.35 ccxt==4.3.50
cryptography==42.0.7 cryptography==42.0.8
aiohttp==3.9.5 aiohttp==3.9.5
SQLAlchemy==2.0.30 SQLAlchemy==2.0.31
python-telegram-bot==21.2 python-telegram-bot==21.3
# can't be hard-pinned due to telegram-bot pinning httpx with ~ # can't be hard-pinned due to telegram-bot pinning httpx with ~
httpx>=0.24.1 httpx>=0.24.1
humanize==4.9.0 humanize==4.9.0
cachetools==5.3.3 cachetools==5.3.3
requests==2.32.2 requests==2.32.3
urllib3==2.2.1 urllib3==2.2.2
jsonschema==4.22.0 jsonschema==4.22.0
TA-Lib==0.4.29 TA-Lib==0.4.31
technical==1.4.3 technical==1.4.3
tabulate==0.9.0 tabulate==0.9.0
pycoingecko==3.1.0 pycoingecko==3.1.0
@@ -30,18 +32,18 @@ py_find_1st==1.1.6
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.17 python-rapidjson==1.17
# Properly format api responses # Properly format api responses
orjson==3.10.3 orjson==3.10.5
# Notify systemd # Notify systemd
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.111.0 fastapi==0.111.0
pydantic==2.7.1 pydantic==2.7.4
uvicorn==0.29.0 uvicorn==0.30.1
pyjwt==2.8.0 pyjwt==2.8.0
aiofiles==23.2.1 aiofiles==23.2.1
psutil==5.9.8 psutil==6.0.0
# Support for colorized terminal output # Support for colorized terminal output
colorama==0.4.6 colorama==0.4.6
@@ -60,4 +62,4 @@ websockets==12.0
janus==1.0.0 janus==1.0.0
ast-comments==1.2.2 ast-comments==1.2.2
packaging==24.0 packaging==24.1
+285
View File
@@ -0,0 +1,285 @@
Clear-Host
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$Global:LogFilePath = Join-Path $env:TEMP "script_log_$Timestamp.txt"
$RequirementFiles = @("requirements.txt", "requirements-dev.txt", "requirements-hyperopt.txt", "requirements-freqai.txt", "requirements-freqai-rl.txt", "requirements-plot.txt")
$VenvName = ".venv"
$VenvDir = Join-Path $PSScriptRoot $VenvName
function Write-Log {
param (
[string]$Message,
[string]$Level = 'INFO'
)
if (-not (Test-Path -Path $LogFilePath)) {
New-Item -ItemType File -Path $LogFilePath -Force | Out-Null
}
switch ($Level) {
'INFO' { Write-Host $Message -ForegroundColor Green }
'WARNING' { Write-Host $Message -ForegroundColor Yellow }
'ERROR' { Write-Host $Message -ForegroundColor Red }
'PROMPT' { Write-Host $Message -ForegroundColor Cyan }
}
"${Level}: $Message" | Out-File $LogFilePath -Append
}
function Get-UserSelection {
param (
[string]$Prompt,
[string[]]$Options,
[string]$DefaultChoice = 'A',
[bool]$AllowMultipleSelections = $true
)
Write-Log "$Prompt`n" -Level 'PROMPT'
for ($I = 0; $I -lt $Options.Length; $I++) {
Write-Log "$([char](65 + $I)). $($Options[$I])" -Level 'PROMPT'
}
if ($AllowMultipleSelections) {
Write-Log "`nSelect one or more options by typing the corresponding letters, separated by commas." -Level 'PROMPT'
}
else {
Write-Log "`nSelect an option by typing the corresponding letter." -Level 'PROMPT'
}
[string]$UserInput = Read-Host
if ([string]::IsNullOrEmpty($UserInput)) {
$UserInput = $DefaultChoice
}
$UserInput = $UserInput.ToUpper()
if ($AllowMultipleSelections) {
$Selections = $UserInput.Split(',') | ForEach-Object { $_.Trim() }
$SelectedIndices = @()
foreach ($Selection in $Selections) {
if ($Selection -match '^[A-Z]$') {
$Index = [int][char]$Selection - [int][char]'A'
if ($Index -ge 0 -and $Index -lt $Options.Length) {
$SelectedIndices += $Index
}
else {
Write-Log "Invalid input: $Selection. Please enter letters within the valid range of options." -Level 'ERROR'
return -1
}
}
else {
Write-Log "Invalid input: $Selection. Please enter a letter between A and Z." -Level 'ERROR'
return -1
}
}
return $SelectedIndices
}
else {
if ($UserInput -match '^[A-Z]$') {
$SelectedIndex = [int][char]$UserInput - [int][char]'A'
if ($SelectedIndex -ge 0 -and $SelectedIndex -lt $Options.Length) {
return $SelectedIndex
}
else {
Write-Log "Invalid input: $UserInput. Please enter a letter within the valid range of options." -Level 'ERROR'
return -1
}
}
else {
Write-Log "Invalid input: $UserInput. Please enter a letter between A and Z." -Level 'ERROR'
return -1
}
}
}
function Exit-Script {
param (
[int]$ExitCode,
[bool]$WaitForKeypress = $true
)
if ($ExitCode -ne 0) {
Write-Log "Script failed. Would you like to open the log file? (Y/N)" -Level 'PROMPT'
$openLog = Read-Host
if ($openLog -eq 'Y' -or $openLog -eq 'y') {
Start-Process notepad.exe -ArgumentList $LogFilePath
}
}
elseif ($WaitForKeypress) {
Write-Log "Press any key to exit..."
$host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | Out-Null
}
return $ExitCode
}
function Test-PythonExecutable {
param(
[string]$PythonExecutable
)
$DeactivateVenv = Join-Path $VenvDir "Scripts\Deactivate.bat"
if (Test-Path $DeactivateVenv) {
Write-Host "Deactivating virtual environment..." 2>&1 | Out-File $LogFilePath -Append
& $DeactivateVenv
Write-Host "Virtual environment deactivated." 2>&1 | Out-File $LogFilePath -Append
}
else {
Write-Host "Deactivation script not found: $DeactivateVenv" 2>&1 | Out-File $LogFilePath -Append
}
$PythonCmd = Get-Command $PythonExecutable -ErrorAction SilentlyContinue
if ($PythonCmd) {
$VersionOutput = & $PythonCmd.Source --version 2>&1
if ($LASTEXITCODE -eq 0) {
$Version = $VersionOutput | Select-String -Pattern "Python (\d+\.\d+\.\d+)" | ForEach-Object { $_.Matches.Groups[1].Value }
Write-Log "Python version $Version found using executable '$PythonExecutable'."
return $true
}
else {
Write-Log "Python executable '$PythonExecutable' not working correctly." -Level 'ERROR'
return $false
}
}
else {
Write-Log "Python executable '$PythonExecutable' not found." -Level 'ERROR'
return $false
}
}
function Find-PythonExecutable {
$PythonExecutables = @(
"python",
"python3.12",
"python3.11",
"python3.10",
"python3.9",
"python3",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python39\python.exe",
"C:\Python312\python.exe",
"C:\Python311\python.exe",
"C:\Python310\python.exe",
"C:\Python39\python.exe"
)
foreach ($Executable in $PythonExecutables) {
if (Test-PythonExecutable -PythonExecutable $Executable) {
return $Executable
}
}
return $null
}
function Main {
"Starting the operations..." | Out-File $LogFilePath -Append
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
# Exit on lower versions than Python 3.9 or when Python executable not found
$PythonExecutable = Find-PythonExecutable
if ($null -eq $PythonExecutable) {
Write-Log "No suitable Python executable found. Please ensure that Python 3.9 or higher is installed and available in the system PATH." -Level 'ERROR'
Exit 1
}
# Define the path to the Python executable in the virtual environment
$ActivateVenv = "$VenvDir\Scripts\Activate.ps1"
# Check if the virtual environment exists, if not, create it
if (-Not (Test-Path $ActivateVenv)) {
Write-Log "Virtual environment not found. Creating virtual environment..." -Level 'ERROR'
& $PythonExecutable -m venv $VenvName 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to create virtual environment." -Level 'ERROR'
Exit-Script -exitCode 1
}
else {
Write-Log "Virtual environment created."
}
}
# Activate the virtual environment and check if it was successful
Write-Log "Virtual environment found. Activating virtual environment..."
& $ActivateVenv 2>&1 | Out-File $LogFilePath -Append
# Check if virtual environment is activated
if ($env:VIRTUAL_ENV) {
Write-Log "Virtual environment is activated at: $($env:VIRTUAL_ENV)"
}
else {
Write-Log "Failed to activate virtual environment." -Level 'ERROR'
Exit-Script -exitCode 1
}
# Ensure pip
python -m ensurepip --default-pip 2>&1 | Out-File $LogFilePath -Append
# Pull latest updates only if the repository state is not dirty
Write-Log "Checking if the repository is clean..."
$Status = & "git" status --porcelain
if ($Status) {
Write-Log "Changes in local git repository. Skipping git pull."
}
else {
Write-Log "Pulling latest updates..."
& "git" pull 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to pull updates from Git." -Level 'ERROR'
Exit-Script -exitCode 1
}
}
if (-not (Test-Path "$VenvDir\Lib\site-packages\talib")) {
# Install TA-Lib using the virtual environment's pip
Write-Log "Installing TA-Lib using virtual environment's pip..."
python -m pip install --find-links=build_helpers\ --prefer-binary TA-Lib 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to install TA-Lib." -Level 'ERROR'
Exit-Script -exitCode 1
}
}
# Present options for requirement files
$SelectedIndices = Get-UserSelection -prompt "Select which requirement files to install:" -options $RequirementFiles -defaultChoice 'A'
# Cache the selected requirement files
$SelectedRequirementFiles = @()
$PipInstallArguments = @()
foreach ($Index in $SelectedIndices) {
$RelativePath = $RequirementFiles[$Index]
if (Test-Path $RelativePath) {
$SelectedRequirementFiles += $RelativePath
$PipInstallArguments += "-r", $RelativePath # Add each flag and path as separate elements
}
else {
Write-Log "Requirement file not found: $RelativePath" -Level 'ERROR'
Exit-Script -exitCode 1
}
}
if ($PipInstallArguments.Count -ne 0) {
& pip install @PipInstallArguments # Use array splatting to pass arguments correctly
}
# Install freqtrade from setup using the virtual environment's Python
Write-Log "Installing freqtrade from setup..."
pip install -e . 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to install freqtrade." -Level 'ERROR'
Exit-Script -exitCode 1
}
Write-Log "Installing freqUI..."
python freqtrade install-ui 2>&1 | Out-File $LogFilePath -Append
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to install freqUI." -Level 'ERROR'
Exit-Script -exitCode 1
}
Write-Log "Installation/Update complete!"
Exit-Script -exitCode 0
}
# Call the Main function
Main
+1 -1
View File
@@ -49,7 +49,7 @@ function updateenv() {
source .venv/bin/activate source .venv/bin/activate
SYS_ARCH=$(uname -m) SYS_ARCH=$(uname -m)
echo "pip install in-progress. Please wait..." echo "pip install in-progress. Please wait..."
${PYTHON} -m pip install --upgrade pip wheel setuptools ${PYTHON} -m pip install --upgrade "pip<=24.0" wheel setuptools
REQUIREMENTS_HYPEROPT="" REQUIREMENTS_HYPEROPT=""
REQUIREMENTS_PLOT="" REQUIREMENTS_PLOT=""
REQUIREMENTS_FREQAI="" REQUIREMENTS_FREQAI=""
+1 -1
View File
@@ -238,7 +238,6 @@ def patched_configuration_load_config_file(mocker, config) -> None:
def patch_exchange( def patch_exchange(
mocker, api_mock=None, id="binance", mock_markets=True, mock_supported_modes=True mocker, api_mock=None, id="binance", mock_markets=True, mock_supported_modes=True
) -> None: ) -> None:
mocker.patch(f"{EXMS}._load_async_markets", return_value={})
mocker.patch(f"{EXMS}.validate_config", MagicMock()) mocker.patch(f"{EXMS}.validate_config", MagicMock())
mocker.patch(f"{EXMS}.validate_timeframes", MagicMock()) mocker.patch(f"{EXMS}.validate_timeframes", MagicMock())
mocker.patch(f"{EXMS}.id", PropertyMock(return_value=id)) mocker.patch(f"{EXMS}.id", PropertyMock(return_value=id))
@@ -248,6 +247,7 @@ def patch_exchange(
mocker.patch("freqtrade.exchange.bybit.Bybit.cache_leverage_tiers") mocker.patch("freqtrade.exchange.bybit.Bybit.cache_leverage_tiers")
if mock_markets: if mock_markets:
mocker.patch(f"{EXMS}._load_async_markets", return_value={})
if isinstance(mock_markets, bool): if isinstance(mock_markets, bool):
mock_markets = get_markets() mock_markets = get_markets()
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=mock_markets)) mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=mock_markets))
+6
View File
@@ -83,6 +83,12 @@ def test_download_data_main_trades(mocker):
assert dl_mock.call_count == 1 assert dl_mock.call_count == 1
assert convert_mock.call_count == 1 assert convert_mock.call_count == 1
# Exchange that doesn't support historic downloads
config["exchange"]["name"] = "bybit"
with pytest.raises(OperationalException, match=r"Trade history not available for .*"):
config
download_data_main(config)
def test_download_data_main_data_invalid(mocker): def test_download_data_main_data_invalid(mocker):
patch_exchange(mocker, id="kraken") patch_exchange(mocker, id="kraken")
+4 -4
View File
@@ -67,10 +67,10 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
"enter_tag_long_b", "enter_tag_long_b",
], ],
"exit_reason": [ "exit_reason": [
ExitType.ROI, ExitType.ROI.value,
ExitType.EXIT_SIGNAL, ExitType.EXIT_SIGNAL.value,
ExitType.STOP_LOSS, ExitType.STOP_LOSS.value,
ExitType.TRAILING_STOP_LOSS, ExitType.TRAILING_STOP_LOSS.value,
], ],
} }
) )
+58 -59
View File
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
import ccxt import ccxt
import pytest import pytest
from numpy import NaN from numpy import nan
from pandas import DataFrame from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode
@@ -181,7 +181,7 @@ def test_remove_exchange_credentials(default_conf) -> None:
def test_init_ccxt_kwargs(default_conf, mocker, caplog): def test_init_ccxt_kwargs(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
aei_mock = mocker.patch(f"{EXMS}.additional_exchange_init") aei_mock = mocker.patch(f"{EXMS}.additional_exchange_init")
@@ -518,7 +518,7 @@ def test__load_async_markets(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt") mocker.patch(f"{EXMS}._init_ccxt")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
exchange = Exchange(default_conf) exchange = Exchange(default_conf)
@@ -527,28 +527,26 @@ def test__load_async_markets(default_conf, mocker, caplog):
assert exchange._api_async.load_markets.call_count == 1 assert exchange._api_async.load_markets.call_count == 1
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) exchange._api_async.load_markets = get_mock_coro(side_effect=ccxt.BaseError("deadbeef"))
exchange._load_async_markets() with pytest.raises(ccxt.BaseError, match="deadbeef"):
exchange._load_async_markets()
assert log_has("Could not load async markets. Reason: deadbeef", caplog)
def test__load_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) api_mock.load_markets = get_mock_coro(side_effect=ccxt.BaseError("SomeError"))
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf) Exchange(default_conf)
assert log_has("Unable to initialize markets.", caplog) assert log_has("Could not load markets.", caplog)
expected_return = {"ETH/BTC": "available"} expected_return = {"ETH/BTC": "available"}
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=expected_return) api_mock.load_markets = get_mock_coro(return_value=expected_return)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
default_conf["exchange"]["pair_whitelist"] = ["ETH/BTC"] default_conf["exchange"]["pair_whitelist"] = ["ETH/BTC"]
ex = Exchange(default_conf) ex = Exchange(default_conf)
@@ -563,12 +561,12 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
start_dt = dt_now() start_dt = dt_now()
time_machine.move_to(start_dt, tick=False) time_machine.move_to(start_dt, tick=False)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value=initial_markets) api_mock.load_markets = get_mock_coro(return_value=initial_markets)
default_conf["exchange"]["markets_refresh_interval"] = 10 default_conf["exchange"]["markets_refresh_interval"] = 10
exchange = get_patched_exchange( exchange = get_patched_exchange(
mocker, default_conf, api_mock, id="binance", mock_markets=False mocker, default_conf, api_mock, id="binance", mock_markets=False
) )
exchange._load_async_markets = MagicMock() lam_spy = mocker.spy(exchange, "_load_async_markets")
assert exchange._last_markets_refresh == dt_ts() assert exchange._last_markets_refresh == dt_ts()
assert exchange.markets == initial_markets assert exchange.markets == initial_markets
@@ -577,42 +575,45 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
# less than 10 minutes have passed, no reload # less than 10 minutes have passed, no reload
exchange.reload_markets() exchange.reload_markets()
assert exchange.markets == initial_markets assert exchange.markets == initial_markets
assert exchange._load_async_markets.call_count == 0 assert lam_spy.call_count == 0
api_mock.load_markets = MagicMock(return_value=updated_markets) api_mock.load_markets = get_mock_coro(return_value=updated_markets)
# more than 10 minutes have passed, reload is executed # more than 10 minutes have passed, reload is executed
time_machine.move_to(start_dt + timedelta(minutes=11), tick=False) time_machine.move_to(start_dt + timedelta(minutes=11), tick=False)
exchange.reload_markets() exchange.reload_markets()
assert exchange.markets == updated_markets assert exchange.markets == updated_markets
assert exchange._load_async_markets.call_count == 1 assert lam_spy.call_count == 1
assert log_has("Performing scheduled market reload..", caplog) assert log_has("Performing scheduled market reload..", caplog)
# Not called again # Not called again
exchange._load_async_markets.reset_mock() lam_spy.reset_mock()
exchange.reload_markets() exchange.reload_markets()
assert exchange._load_async_markets.call_count == 0 assert lam_spy.call_count == 0
def test_reload_markets_exception(default_conf, mocker, caplog): def test_reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError")) api_mock.load_markets = get_mock_coro(side_effect=ccxt.NetworkError("LoadError"))
default_conf["exchange"]["markets_refresh_interval"] = 10 default_conf["exchange"]["markets_refresh_interval"] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange = get_patched_exchange(
mocker, default_conf, api_mock, id="binance", mock_markets=False
)
exchange._last_markets_refresh = 2
# less than 10 minutes have passed, no reload # less than 10 minutes have passed, no reload
exchange.reload_markets() exchange.reload_markets()
assert exchange._last_markets_refresh == 0 assert exchange._last_markets_refresh == 2
assert log_has_re(r"Could not reload markets.*", caplog) assert log_has_re(r"Could not load markets\..*", caplog)
@pytest.mark.parametrize("stake_currency", ["ETH", "BTC", "USDT"]) @pytest.mark.parametrize("stake_currency", ["ETH", "BTC", "USDT"])
def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
default_conf["stake_currency"] = stake_currency default_conf["stake_currency"] = stake_currency
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).load_markets = MagicMock( type(api_mock).load_markets = get_mock_coro(
return_value={ return_value={
"ETH/BTC": {"quote": "BTC"}, "ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"}, "LTC/BTC": {"quote": "BTC"},
@@ -623,7 +624,6 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf) Exchange(default_conf)
@@ -631,7 +631,7 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
def test_validate_stakecurrency_error(default_conf, mocker, caplog): def test_validate_stakecurrency_error(default_conf, mocker, caplog):
default_conf["stake_currency"] = "XRP" default_conf["stake_currency"] = "XRP"
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).load_markets = MagicMock( type(api_mock).load_markets = get_mock_coro(
return_value={ return_value={
"ETH/BTC": {"quote": "BTC"}, "ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"}, "LTC/BTC": {"quote": "BTC"},
@@ -642,14 +642,13 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
with pytest.raises( with pytest.raises(
ConfigurationError, ConfigurationError,
match=r"XRP is not available as stake on .*Available currencies are: BTC, ETH, USDT", match=r"XRP is not available as stake on .*Available currencies are: BTC, ETH, USDT",
): ):
Exchange(default_conf) Exchange(default_conf)
type(api_mock).load_markets = MagicMock(side_effect=ccxt.NetworkError("No connection.")) type(api_mock).load_markets = get_mock_coro(side_effect=ccxt.NetworkError("No connection."))
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
with pytest.raises( with pytest.raises(
@@ -694,24 +693,26 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
assert ex.get_pair_base_currency(pair) == expected assert ex.get_pair_base_currency(pair) == expected
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).load_markets = MagicMock(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
}
)
id_mock = PropertyMock(return_value="test_exchange") id_mock = PropertyMock(return_value="test_exchange")
type(api_mock).id = id_mock type(api_mock).id = id_mock
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets") mocker.patch(
f"{EXMS}._load_async_markets",
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
},
)
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
# test exchange.validate_pairs directly
# No assert - but this should not fail (!)
Exchange(default_conf) Exchange(default_conf)
@@ -751,7 +752,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
def test_validate_pairs_restricted(default_conf, mocker, caplog): def test_validate_pairs_restricted(default_conf, mocker, caplog):
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).load_markets = MagicMock( type(api_mock).load_markets = get_mock_coro(
return_value={ return_value={
"ETH/BTC": {"quote": "BTC"}, "ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"}, "LTC/BTC": {"quote": "BTC"},
@@ -761,7 +762,6 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
) )
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
@@ -774,9 +774,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
) )
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).load_markets = MagicMock( type(api_mock).load_markets = get_mock_coro(
return_value={ return_value={
"ETH/BTC": {"quote": "BTC"}, "ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"}, "LTC/BTC": {"quote": "BTC"},
@@ -787,17 +787,16 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
) )
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf) Exchange(default_conf)
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
default_conf["stake_currency"] = "" default_conf["stake_currency"] = ""
type(api_mock).load_markets = MagicMock( type(api_mock).load_markets = get_mock_coro(
return_value={ return_value={
"ETH/BTC": {"quote": "BTC"}, "ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"}, "LTC/BTC": {"quote": "BTC"},
@@ -808,7 +807,6 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
) )
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@@ -816,10 +814,10 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
assert type(api_mock).load_markets.call_count == 1 assert type(api_mock).load_markets.call_count == 1
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility_fail(default_conf, mocker):
default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD") default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD")
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).load_markets = MagicMock( type(api_mock).load_markets = get_mock_coro(
return_value={ return_value={
"ETH/BTC": {"quote": "BTC"}, "ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"}, "LTC/BTC": {"quote": "BTC"},
@@ -830,7 +828,6 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
) )
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"): with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
@@ -847,7 +844,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
type(api_mock).timeframes = timeframes type(api_mock).timeframes = timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@@ -865,7 +862,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
type(api_mock).timeframes = timeframes type(api_mock).timeframes = timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@@ -895,7 +892,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
del api_mock.timeframes del api_mock.timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises( with pytest.raises(
@@ -917,7 +914,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
del api_mock.timeframes del api_mock.timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={"timeframes": None})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs", MagicMock()) mocker.patch(f"{EXMS}.validate_pairs", MagicMock())
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises( with pytest.raises(
@@ -939,7 +936,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
type(api_mock).timeframes = timeframes type(api_mock).timeframes = timeframes
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@@ -955,7 +952,7 @@ def test_validate_pricing(default_conf, mocker):
} }
type(api_mock).has = PropertyMock(return_value=has) type(api_mock).has = PropertyMock(return_value=has)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode") mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
@@ -991,7 +988,7 @@ def test_validate_ordertypes(default_conf, mocker):
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True}) type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
@@ -1050,7 +1047,7 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
default_conf["margin_mode"] = MarginMode.ISOLATED default_conf["margin_mode"] = MarginMode.ISOLATED
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True}) type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
@@ -1075,7 +1072,7 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
def test_validate_order_types_not_in_config(default_conf, mocker): def test_validate_order_types_not_in_config(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={})) mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs") mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@@ -1947,7 +1944,9 @@ def test_fetch_trading_fees(default_conf, mocker):
assert api_mock.fetch_trading_fees.call_count == 1 assert api_mock.fetch_trading_fees.call_count == 1
api_mock.fetch_trading_fees.reset_mock() api_mock.fetch_trading_fees.reset_mock()
# Reload-markets calls fetch_trading_fees, too - so the explicit calls in the below
# exception test would be called twice.
mocker.patch(f"{EXMS}.reload_markets")
ccxt_exceptionhandlers( ccxt_exceptionhandlers(
mocker, default_conf, api_mock, exchange_name, "fetch_trading_fees", "fetch_trading_fees" mocker, default_conf, api_mock, exchange_name, "fetch_trading_fees", "fetch_trading_fees"
) )
@@ -4861,7 +4860,7 @@ def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value,
(10, 0.0001, 2.0, 1.0, 0.002, 0.002), (10, 0.0001, 2.0, 1.0, 0.002, 0.002),
(10, 0.0002, 2.0, 0.01, 0.004, 0.00004), (10, 0.0002, 2.0, 0.01, 0.004, 0.00004),
(10, 0.0002, 2.5, None, 0.005, None), (10, 0.0002, 2.5, None, 0.005, None),
(10, 0.0002, NaN, None, 0.0, None), (10, 0.0002, nan, None, 0.0, None),
], ],
) )
def test_calculate_funding_fees( def test_calculate_funding_fees(
+1
View File
@@ -50,6 +50,7 @@ def freqai_conf(default_conf, tmp_path):
freqaiconf.update( freqaiconf.update(
{ {
"datadir": Path(default_conf["datadir"]), "datadir": Path(default_conf["datadir"]),
"runmode": "backtest",
"strategy": "freqai_test_strat", "strategy": "freqai_test_strat",
"user_data_dir": tmp_path, "user_data_dir": tmp_path,
"strategy-path": "freqtrade/tests/strategy/strats", "strategy-path": "freqtrade/tests/strategy/strats",
+3 -1
View File
@@ -699,18 +699,20 @@ def test_process_trade_creation(
def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> None: def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> None:
# TODO: Move this test to test_worker
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
EXMS, EXMS,
fetch_ticker=ticker_usdt, fetch_ticker=ticker_usdt,
reload_markets=MagicMock(side_effect=TemporaryError), reload_markets=MagicMock(),
create_order=MagicMock(side_effect=TemporaryError), create_order=MagicMock(side_effect=TemporaryError),
) )
sleep_mock = mocker.patch("time.sleep") sleep_mock = mocker.patch("time.sleep")
worker = Worker(args=None, config=default_conf_usdt) worker = Worker(args=None, config=default_conf_usdt)
patch_get_signal(worker.freqtrade) patch_get_signal(worker.freqtrade)
mocker.patch(f"{EXMS}.reload_markets", MagicMock(side_effect=TemporaryError))
worker._process_running() worker._process_running()
assert sleep_mock.called is True assert sleep_mock.called is True
+4 -4
View File
@@ -1650,11 +1650,11 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
] ]
args = get_args(args) args = get_args(args)
start_backtesting(args) start_backtesting(args)
# 2 backtests, 4 tables # 2 backtests, 6 tables (entry, exit, mixed - each 2x)
assert backtestmock.call_count == 2 assert backtestmock.call_count == 2
assert text_table_mock.call_count == 4 assert text_table_mock.call_count == 4
assert strattable_mock.call_count == 1 assert strattable_mock.call_count == 1
assert tag_metrics_mock.call_count == 4 assert tag_metrics_mock.call_count == 6
assert strat_summary.call_count == 1 assert strat_summary.call_count == 1
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
@@ -1709,7 +1709,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
"open_rate": [0.104445, 0.10302485], "open_rate": [0.104445, 0.10302485],
"close_rate": [0.104969, 0.103541], "close_rate": [0.104969, 0.103541],
"is_short": [False, False], "is_short": [False, False],
"exit_reason": [ExitType.ROI, ExitType.ROI], "exit_reason": [ExitType.ROI.value, ExitType.ROI.value],
} }
) )
result2 = pd.DataFrame( result2 = pd.DataFrame(
@@ -1729,7 +1729,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
"open_rate": [0.104445, 0.10302485, 0.122541], "open_rate": [0.104445, 0.10302485, 0.122541],
"close_rate": [0.104969, 0.103541, 0.123541], "close_rate": [0.104969, 0.103541, 0.123541],
"is_short": [False, False, False], "is_short": [False, False, False],
"exit_reason": [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS], "exit_reason": [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value],
} }
) )
backtestmock = MagicMock( backtestmock = MagicMock(
+9 -9
View File
@@ -415,10 +415,10 @@ def test_hyperopt_format_results(hyperopt):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ "exit_reason": [
ExitType.ROI, ExitType.ROI.value,
ExitType.STOP_LOSS, ExitType.STOP_LOSS.value,
ExitType.ROI, ExitType.ROI.value,
ExitType.FORCE_EXIT, ExitType.FORCE_EXIT.value,
], ],
} }
), ),
@@ -507,10 +507,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ "exit_reason": [
ExitType.ROI, ExitType.ROI.value,
ExitType.STOP_LOSS, ExitType.STOP_LOSS.value,
ExitType.ROI, ExitType.ROI.value,
ExitType.FORCE_EXIT, ExitType.FORCE_EXIT.value,
], ],
} }
), ),
@@ -1063,7 +1063,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None: def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None:
mocker.patch(f"{EXMS}.validate_config", MagicMock()) mocker.patch(f"{EXMS}.validate_config", MagicMock())
mocker.patch(f"{EXMS}.get_fee", fee) mocker.patch(f"{EXMS}.get_fee", fee)
mocker.patch(f"{EXMS}._load_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=get_markets())) mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=get_markets()))
(tmp_path / "hyperopt_results").mkdir(parents=True) (tmp_path / "hyperopt_results").mkdir(parents=True)
# Dummy-reduce points to ensure scikit-learn is forced to generate new values # Dummy-reduce points to ensure scikit-learn is forced to generate new values
+17 -17
View File
@@ -70,13 +70,13 @@ def test_text_table_bt_results():
) )
result_str = ( result_str = (
"| Pair | Entries | Avg Profit % | Tot Profit BTC | " "| Pair | Trades | Avg Profit % | Tot Profit BTC | "
"Tot Profit % | Avg Duration | Win Draw Loss Win% |\n" "Tot Profit % | Avg Duration | Win Draw Loss Win% |\n"
"|---------+-----------+----------------+------------------+" "|---------+----------+----------------+------------------+"
"----------------+----------------+-------------------------|\n" "----------------+----------------+-------------------------|\n"
"| ETH/BTC | 3 | 8.33 | 0.50000000 | " "| ETH/BTC | 3 | 8.33 | 0.50000000 | "
"12.50 | 0:20:00 | 2 0 1 66.7 |\n" "12.50 | 0:20:00 | 2 0 1 66.7 |\n"
"| TOTAL | 3 | 8.33 | 0.50000000 | " "| TOTAL | 3 | 8.33 | 0.50000000 | "
"12.50 | 0:20:00 | 2 0 1 66.7 |" "12.50 | 0:20:00 | 2 0 1 66.7 |"
) )
@@ -116,10 +116,10 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ "exit_reason": [
ExitType.ROI, ExitType.ROI.value,
ExitType.STOP_LOSS, ExitType.STOP_LOSS.value,
ExitType.ROI, ExitType.ROI.value,
ExitType.FORCE_EXIT, ExitType.FORCE_EXIT.value,
], ],
} }
), ),
@@ -183,10 +183,10 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
"is_short": [False, False, False, False], "is_short": [False, False, False, False],
"stake_amount": [0.01, 0.01, 0.01, 0.01], "stake_amount": [0.01, 0.01, 0.01, 0.01],
"exit_reason": [ "exit_reason": [
ExitType.ROI, ExitType.ROI.value,
ExitType.ROI, ExitType.ROI.value,
ExitType.STOP_LOSS, ExitType.STOP_LOSS.value,
ExitType.FORCE_EXIT, ExitType.FORCE_EXIT.value,
], ],
} }
), ),
@@ -444,7 +444,7 @@ def test_text_table_exit_reason():
"wins": [2, 0, 0], "wins": [2, 0, 0],
"draws": [0, 0, 0], "draws": [0, 0, 0],
"losses": [0, 0, 1], "losses": [0, 0, 1],
"exit_reason": [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS], "exit_reason": [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value],
} }
) )
@@ -509,13 +509,13 @@ def test_text_table_strategy(testdatadir):
bt_res_data_comparison = bt_res_data.pop("strategy_comparison") bt_res_data_comparison = bt_res_data.pop("strategy_comparison")
result_str = ( result_str = (
"| Strategy | Entries | Avg Profit % | Tot Profit BTC |" "| Strategy | Trades | Avg Profit % | Tot Profit BTC |"
" Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n" " Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n"
"|----------------+-----------+----------------+------------------+" "|----------------+----------+----------------+------------------+"
"----------------+----------------+-------------------------+-----------------------|\n" "----------------+----------------+-------------------------+-----------------------|\n"
"| StrategyTestV2 | 179 | 0.08 | 0.02608550 |" "| StrategyTestV2 | 179 | 0.08 | 0.02608550 |"
" 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n" " 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n"
"| TestStrategy | 179 | 0.08 | 0.02608550 |" "| TestStrategy | 179 | 0.08 | 0.02608550 |"
" 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |" " 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |"
) )
+2
View File
@@ -773,6 +773,7 @@ def test_VolumePairList_whitelist_gen(
whitelist_result, whitelist_result,
caplog, caplog,
) -> None: ) -> None:
whitelist_conf["runmode"] = "backtest"
whitelist_conf["pairlists"] = pairlists whitelist_conf["pairlists"] = pairlists
whitelist_conf["stake_currency"] = base_currency whitelist_conf["stake_currency"] = base_currency
@@ -1270,6 +1271,7 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None:
{"method": "StaticPairList"}, {"method": "StaticPairList"},
{"method": "ShuffleFilter", "seed": 43}, {"method": "ShuffleFilter", "seed": 43},
] ]
whitelist_conf["runmode"] = "backtest"
exchange = get_patched_exchange(mocker, whitelist_conf) exchange = get_patched_exchange(mocker, whitelist_conf)
plm = PairListManager(exchange, whitelist_conf) plm = PairListManager(exchange, whitelist_conf)
+177
View File
@@ -0,0 +1,177 @@
Describe "Setup and Tests" {
BeforeAll {
# Setup variables
$SetupScriptPath = Join-Path $PSScriptRoot "..\setup.ps1"
$Global:LogFilePath = Join-Path $env:TEMP "script_log.txt"
# Check if the setup script exists
if (-Not (Test-Path -Path $SetupScriptPath)) {
Write-Host "Error: setup.ps1 script not found at path: $SetupScriptPath"
exit 1
}
# Mock main to prevent it from running
Mock Main {}
. $SetupScriptPath
}
Context "Write-Log Tests" -Tag "Unit" {
It "should write INFO level log" {
if (Test-Path $Global:LogFilePath){
Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue
}
Write-Log -Message "Test Info Message" -Level "INFO"
$Global:LogFilePath | Should -Exist
$LogContent = Get-Content $Global:LogFilePath
$LogContent | Should -Contain "INFO: Test Info Message"
}
It "should write ERROR level log" {
if (Test-Path $Global:LogFilePath){
Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue
}
Write-Log -Message "Test Error Message" -Level "ERROR"
$Global:LogFilePath | Should -Exist
$LogContent = Get-Content $Global:LogFilePath
$LogContent | Should -Contain "ERROR: Test Error Message"
}
}
Describe "Get-UserSelection Tests" {
Context "Valid input" {
It "Should return the correct index for a valid single selection" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "B" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be 1
}
It "Should return the correct index for a valid single selection" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "b" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be 1
}
It "Should return the default choice when no input is provided" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options -defaultChoice "C"
$Result | Should -Be 2
}
}
Context "Invalid input" {
It "Should return -1 for an invalid letter selection" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "X" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be -1
}
It "Should return -1 for a selection outside the valid range" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "D" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be -1
}
It "Should return -1 for a non-letter input" {
$Options = @("Option1", "Option2", "Option3")
Mock Read-Host { return "1" }
$Result = Get-UserSelection -prompt "Select an option" -options $Options
$Result | Should -Be -1
}
It "Should return -1 for mixed valid and invalid input" {
Mock Read-Host { return "A,X,B,Y,C,Z" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
$Indices | Should -Be -1
}
}
Context "Multiple selections" {
It "Should handle valid input correctly" {
Mock Read-Host { return "A, B, C" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
$Indices | Should -Be @(0, 1, 2)
}
It "Should handle valid input without whitespace correctly" {
Mock Read-Host { return "A,B,C" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
$Indices | Should -Be @(0, 1, 2)
}
It "Should return indices for selected options" {
Mock Read-Host { return "a,b" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options
$Indices | Should -Be @(0, 1)
}
It "Should return default choice if no input" {
Mock Read-Host { return "" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "C"
$Indices | Should -Be @(2)
}
It "Should handle invalid input gracefully" {
Mock Read-Host { return "x,y,z" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
$Indices | Should -Be -1
}
It "Should handle input without whitespace" {
Mock Read-Host { return "a,b,c" }
$Options = @("Option1", "Option2", "Option3")
$Indices = Get-UserSelection -prompt "Select options" -options $Options
$Indices | Should -Be @(0, 1, 2)
}
}
}
Describe "Exit-Script Tests" -Tag "Unit" {
BeforeEach {
Mock Write-Log {}
Mock Start-Process {}
Mock Read-Host { return "Y" }
}
It "should exit with the given exit code without waiting for key press" {
$ExitCode = Exit-Script -ExitCode 0 -isSubShell $true -waitForKeypress $false
$ExitCode | Should -Be 0
}
It "should prompt to open log file on error" {
Exit-Script -ExitCode 1 -isSubShell $true -waitForKeypress $false
Assert-MockCalled Read-Host -Exactly 1
Assert-MockCalled Start-Process -Exactly 1
}
}
Context 'Find-PythonExecutable' {
It 'Returns the first valid Python executable' {
Mock Test-PythonExecutable { $true } -ParameterFilter { $PythonExecutable -eq 'python' }
$Result = Find-PythonExecutable
$Result | Should -Be 'python'
}
It 'Returns null if no valid Python executable is found' {
Mock Test-PythonExecutable { $false }
$Result = Find-PythonExecutable
$Result | Should -Be $null
}
}
}
+40 -13
View File
@@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
import logging import logging
import math
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
@@ -458,55 +459,66 @@ def test_min_roi_reached3(default_conf, fee) -> None:
ExitType.TRAILING_STOP_LOSS, ExitType.TRAILING_STOP_LOSS,
None, None,
), ),
(0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 1, ExitType.NONE, None), (0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 0.998, ExitType.NONE, None),
(0.05, 1, ExitType.NONE, None, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None), (
0.05,
0.998,
ExitType.NONE,
None,
True,
False,
-0.01,
0.998,
ExitType.TRAILING_STOP_LOSS,
None,
),
# Default custom case - trails with 10% # Default custom case - trails with 10%
(0.05, 0.95, ExitType.NONE, None, False, True, -0.02, 0.95, ExitType.NONE, None), (0.05, 0.945, ExitType.NONE, None, False, True, -0.02, 0.945, ExitType.NONE, None),
( (
0.05, 0.05,
0.95, 0.945,
ExitType.NONE, ExitType.NONE,
None, None,
False, False,
True, True,
-0.06, -0.06,
0.95, 0.945,
ExitType.TRAILING_STOP_LOSS, ExitType.TRAILING_STOP_LOSS,
None, None,
), ),
( (
0.05, 0.05,
1, 0.998,
ExitType.NONE, ExitType.NONE,
None, None,
False, False,
True, True,
-0.06, -0.06,
1, 0.998,
ExitType.TRAILING_STOP_LOSS, ExitType.TRAILING_STOP_LOSS,
lambda **kwargs: -0.05, lambda **kwargs: -0.05,
), ),
( (
0.05, 0.05,
1, 0.998,
ExitType.NONE, ExitType.NONE,
None, None,
False, False,
True, True,
0.09, 0.09,
1.04, 1.036,
ExitType.NONE, ExitType.NONE,
lambda **kwargs: -0.05, lambda **kwargs: -0.05,
), ),
( (
0.05, 0.05,
0.95, 0.945,
ExitType.NONE, ExitType.NONE,
None, None,
False, False,
True, True,
0.09, 0.09,
0.98, 0.981,
ExitType.NONE, ExitType.NONE,
lambda current_profit, **kwargs: ( lambda current_profit, **kwargs: (
-0.1 if current_profit < 0.6 else -(current_profit * 2) -0.1 if current_profit < 0.6 else -(current_profit * 2)
@@ -525,6 +537,19 @@ def test_min_roi_reached3(default_conf, fee) -> None:
ExitType.NONE, ExitType.NONE,
lambda **kwargs: None, lambda **kwargs: None,
), ),
# Error case - Returning inf.
(
0.05,
0.9,
ExitType.NONE,
None,
False,
True,
0.09,
0.9,
ExitType.NONE,
lambda **kwargs: math.inf,
),
], ],
) )
def test_ft_stoploss_reached( def test_ft_stoploss_reached(
@@ -552,6 +577,8 @@ def test_ft_stoploss_reached(
exchange="binance", exchange="binance",
open_rate=1, open_rate=1,
liquidation_price=liq, liquidation_price=liq,
price_precision=4,
precision_mode=2,
) )
trade.adjust_min_max_rates(trade.open_rate, trade.open_rate) trade.adjust_min_max_rates(trade.open_rate, trade.open_rate)
strategy.trailing_stop = trailing strategy.trailing_stop = trailing
@@ -577,7 +604,7 @@ def test_ft_stoploss_reached(
assert sl_flag.exit_flag is False assert sl_flag.exit_flag is False
else: else:
assert sl_flag.exit_flag is True assert sl_flag.exit_flag is True
assert round(trade.stop_loss, 2) == adjusted assert round(trade.stop_loss, 3) == adjusted
current_rate2 = trade.open_rate * (1 + profit2) current_rate2 = trade.open_rate * (1 + profit2)
sl_flag = strategy.ft_stoploss_reached( sl_flag = strategy.ft_stoploss_reached(
@@ -593,7 +620,7 @@ def test_ft_stoploss_reached(
assert sl_flag.exit_flag is False assert sl_flag.exit_flag is False
else: else:
assert sl_flag.exit_flag is True assert sl_flag.exit_flag is True
assert round(trade.stop_loss, 2) == adjusted2 assert round(trade.stop_loss, 3) == adjusted2
strategy.custom_stoploss = original_stopvalue strategy.custom_stoploss = original_stopvalue
+6 -6
View File
@@ -401,11 +401,11 @@ def test_load_dry_run(default_conf, mocker, config_value, expected, arglist) ->
assert validated_conf["runmode"] == (RunMode.DRY_RUN if expected else RunMode.LIVE) assert validated_conf["runmode"] == (RunMode.DRY_RUN if expected else RunMode.LIVE)
def test_load_custom_strategy(default_conf, mocker) -> None: def test_load_custom_strategy(default_conf, mocker, tmp_path) -> None:
default_conf.update( default_conf.update(
{ {
"strategy": "CustomStrategy", "strategy": "CustomStrategy",
"strategy_path": "/tmp/strategies", "strategy_path": f"{tmp_path}/strategies",
} }
) )
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
@@ -415,7 +415,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert validated_conf.get("strategy") == "CustomStrategy" assert validated_conf.get("strategy") == "CustomStrategy"
assert validated_conf.get("strategy_path") == "/tmp/strategies" assert validated_conf.get("strategy_path") == f"{tmp_path}/strategies"
def test_show_info(default_conf, mocker, caplog) -> None: def test_show_info(default_conf, mocker, caplog) -> None:
@@ -469,7 +469,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert "timerange" not in config assert "timerange" not in config
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_arguments(mocker, default_conf, caplog, tmp_path) -> None:
patched_configuration_load_config_file(mocker, default_conf) patched_configuration_load_config_file(mocker, default_conf)
mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x) mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x)
mocker.patch( mocker.patch(
@@ -485,7 +485,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
"--datadir", "--datadir",
"/foo/bar", "/foo/bar",
"--userdir", "--userdir",
"/tmp/freqtrade", f"{tmp_path}/freqtrade",
"--timeframe", "--timeframe",
"1m", "1m",
"--enable-position-stacking", "--enable-position-stacking",
@@ -509,7 +509,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert "pair_whitelist" in config["exchange"] assert "pair_whitelist" in config["exchange"]
assert "datadir" in config assert "datadir" in config
assert log_has("Using data directory: {} ...".format("/foo/bar"), caplog) assert log_has("Using data directory: {} ...".format("/foo/bar"), caplog)
assert log_has("Using user-data directory: {} ...".format(Path("/tmp/freqtrade")), caplog) assert log_has(f"Using user-data directory: {tmp_path / 'freqtrade'} ...", caplog)
assert "user_data_dir" in config assert "user_data_dir" in config
assert "timeframe" in config assert "timeframe" in config
+21 -27
View File
@@ -24,16 +24,16 @@ def test_create_datadir(mocker, default_conf, caplog) -> None:
assert log_has("Created data directory: /foo/bar", caplog) assert log_has("Created data directory: /foo/bar", caplog)
def test_create_userdata_dir(mocker, default_conf, caplog) -> None: def test_create_userdata_dir(mocker, tmp_path, caplog) -> None:
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
md = mocker.patch.object(Path, "mkdir", MagicMock()) md = mocker.patch.object(Path, "mkdir", MagicMock())
x = create_userdata_dir("/tmp/bar", create_dir=True) x = create_userdata_dir(tmp_path / "bar", create_dir=True)
assert md.call_count == 10 assert md.call_count == 10
assert md.call_args[1]["parents"] is False assert md.call_args[1]["parents"] is False
assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog) assert log_has(f'Created user-data directory: {tmp_path / "bar"}', caplog)
assert isinstance(x, Path) assert isinstance(x, Path)
assert str(x) == str(Path("/tmp/bar")) assert str(x) == str(tmp_path / "bar")
def test_create_userdata_dir_and_chown(mocker, tmp_path, caplog) -> None: def test_create_userdata_dir_and_chown(mocker, tmp_path, caplog) -> None:
@@ -54,63 +54,57 @@ def test_create_userdata_dir_and_chown(mocker, tmp_path, caplog) -> None:
del os.environ["FT_APP_ENV"] del os.environ["FT_APP_ENV"]
def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None: def test_create_userdata_dir_exists(mocker, tmp_path) -> None:
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
md = mocker.patch.object(Path, "mkdir", MagicMock()) md = mocker.patch.object(Path, "mkdir", MagicMock())
create_userdata_dir("/tmp/bar") create_userdata_dir(f"{tmp_path}/bar")
assert md.call_count == 0 assert md.call_count == 0
def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None: def test_create_userdata_dir_exists_exception(mocker, tmp_path) -> None:
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
md = mocker.patch.object(Path, "mkdir", MagicMock()) md = mocker.patch.object(Path, "mkdir", MagicMock())
with pytest.raises( with pytest.raises(OperationalException, match=r"Directory `.*.{1,2}bar` does not exist.*"):
OperationalException, match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist.*" create_userdata_dir(f"{tmp_path}/bar", create_dir=False)
):
create_userdata_dir("/tmp/bar", create_dir=False)
assert md.call_count == 0 assert md.call_count == 0
def test_copy_sample_files(mocker, default_conf, caplog) -> None: def test_copy_sample_files(mocker, tmp_path) -> None:
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
mocker.patch.object(Path, "exists", MagicMock(return_value=False)) mocker.patch.object(Path, "exists", MagicMock(return_value=False))
copymock = mocker.patch("shutil.copy", MagicMock()) copymock = mocker.patch("shutil.copy", MagicMock())
copy_sample_files(Path("/tmp/bar")) copy_sample_files(Path(f"{tmp_path}/bar"))
assert copymock.call_count == 3 assert copymock.call_count == 3
assert copymock.call_args_list[0][0][1] == str( assert copymock.call_args_list[0][0][1] == str(tmp_path / "bar/strategies/sample_strategy.py")
Path("/tmp/bar") / "strategies/sample_strategy.py"
)
assert copymock.call_args_list[1][0][1] == str( assert copymock.call_args_list[1][0][1] == str(
Path("/tmp/bar") / "hyperopts/sample_hyperopt_loss.py" tmp_path / "bar/hyperopts/sample_hyperopt_loss.py"
) )
assert copymock.call_args_list[2][0][1] == str( assert copymock.call_args_list[2][0][1] == str(
Path("/tmp/bar") / "notebooks/strategy_analysis_example.ipynb" tmp_path / "bar/notebooks/strategy_analysis_example.ipynb"
) )
def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None: def test_copy_sample_files_errors(mocker, tmp_path, caplog) -> None:
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False)) mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
mocker.patch.object(Path, "exists", MagicMock(return_value=False)) mocker.patch.object(Path, "exists", MagicMock(return_value=False))
mocker.patch("shutil.copy", MagicMock()) mocker.patch("shutil.copy", MagicMock())
with pytest.raises( with pytest.raises(OperationalException, match=r"Directory `.*.{1,2}bar` does not exist\."):
OperationalException, match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist\." copy_sample_files(Path(f"{tmp_path}/bar"))
):
copy_sample_files(Path("/tmp/bar"))
mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False])) mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False]))
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
match=r"Directory `.{1,2}tmp.{1,2}bar.{1,2}strategies` does not exist\.", match=r"Directory `.*.{1,2}bar.{1,2}strategies` does not exist\.",
): ):
copy_sample_files(Path("/tmp/bar")) copy_sample_files(Path(f"{tmp_path}/bar"))
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True)) mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "exists", MagicMock(return_value=True))
copy_sample_files(Path("/tmp/bar")) copy_sample_files(Path(f"{tmp_path}/bar"))
assert log_has_re(r"File `.*` exists already, not deploying sample file\.", caplog) assert log_has_re(r"File `.*` exists already, not deploying sample file\.", caplog)
caplog.clear() caplog.clear()
copy_sample_files(Path("/tmp/bar"), overwrite=True) copy_sample_files(Path(f"{tmp_path}/bar"), overwrite=True)
assert log_has_re(r"File `.*` exists already, overwriting\.", caplog) assert log_has_re(r"File `.*` exists already, overwriting\.", caplog)