mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-09-20 09:31:12 +00:00
Merge pull request #10383 from freqtrade/new_release
New release 2024.6
This commit is contained in:
commit
a55691ea7f
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
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"]
|
||||
|
||||
steps:
|
||||
|
@ -55,7 +55,7 @@ jobs:
|
|||
|
||||
- name: Installation - *nix
|
||||
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 TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
|
@ -192,7 +192,7 @@ jobs:
|
|||
|
||||
- name: Installation (python)
|
||||
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 TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
|
@ -318,6 +318,17 @@ jobs:
|
|||
run: |
|
||||
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
|
||||
uses: rjstone/discord-webhook-notify@v1
|
||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||
|
@ -411,7 +422,7 @@ jobs:
|
|||
|
||||
- name: Installation - *nix
|
||||
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 TA_LIBRARY_PATH=${HOME}/dependencies/lib
|
||||
export TA_INCLUDE_PATH=${HOME}/dependencies/include
|
||||
|
@ -522,12 +533,12 @@ jobs:
|
|||
|
||||
|
||||
- name: Publish to PyPI (Test)
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.14
|
||||
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.14
|
||||
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||
|
||||
|
||||
deploy-docker:
|
||||
|
@ -566,11 +577,11 @@ jobs:
|
|||
docker version -f '{{.Server.Experimental}}'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: "7.0.0"
|
||||
rev: "7.1.0"
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [Flake8-pyproject]
|
||||
|
@ -16,10 +16,10 @@ repos:
|
|||
additional_dependencies:
|
||||
- types-cachetools==5.3.0.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-python-dateutil==2.9.0.20240316
|
||||
- SQLAlchemy==2.0.30
|
||||
- SQLAlchemy==2.0.31
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
|
@ -31,7 +31,7 @@ repos:
|
|||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.4.5'
|
||||
rev: 'v0.4.10'
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.12.3-slim-bookworm as base
|
||||
FROM python:3.12.4-slim-bookworm as base
|
||||
|
||||
# Setup env
|
||||
ENV LANG C.UTF-8
|
||||
|
@ -25,7 +25,7 @@ FROM base as python-deps
|
|||
RUN apt-get update \
|
||||
&& apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
|
||||
&& apt-get clean \
|
||||
&& pip install --upgrade pip wheel
|
||||
&& pip install --upgrade "pip<=24.0" wheel
|
||||
|
||||
# Install TA-lib
|
||||
COPY build_helpers/* /tmp/
|
||||
|
@ -35,7 +35,7 @@ ENV LD_LIBRARY_PATH /usr/local/lib
|
|||
# Install dependencies
|
||||
COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
|
||||
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
|
||||
|
||||
# Copy dependencies to runtime-image
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.31-cp310-cp310-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.31-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.31-cp311-cp311-linux_armv7l.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.31-cp311-cp311-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.31-cp311-cp311-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.31-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.31-cp312-cp312-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.31-cp312-cp312-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.31-cp39-cp39-linux_armv7l.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.31-cp39-cp39-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.31-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.31-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
# 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}')"
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ RUN mkdir /freqtrade \
|
|||
&& chown ftuser:ftuser /freqtrade \
|
||||
# Allow sudoers
|
||||
&& echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers \
|
||||
&& pip install --upgrade pip
|
||||
&& pip install --upgrade "pip<=24.0"
|
||||
|
||||
WORKDIR /freqtrade
|
||||
|
||||
|
|
|
@ -253,36 +253,36 @@ A backtesting result will look like that:
|
|||
|
||||
```
|
||||
================================================ BACKTESTING REPORT =================================================
|
||||
| Pair | Entries | 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
============================================= LEFT OPEN TRADES REPORT =============================================
|
||||
| Pair | Entries | 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
==================== EXIT REASON STATS ====================
|
||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||
|:-------------------|--------:|------:|-------:|--------:|
|
||||
|--------------------+---------+-------+--------+---------|
|
||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||
| stop_loss | 166 | 0 | 0 | 166 |
|
||||
| exit_signal | 56 | 36 | 0 | 20 |
|
||||
|
@ -631,10 +631,10 @@ Detailed output for all strategies one after the other will be available, so mak
|
|||
|
||||
```
|
||||
================================================== STRATEGY SUMMARY ===================================================================
|
||||
| Strategy | Entries | 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 |
|
||||
| Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
|
||||
| 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 |
|
||||
| Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
|
||||
```
|
||||
|
||||
## Next step
|
||||
|
|
|
@ -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.
|
||||
|
||||
!!! 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.
|
||||
|
||||
!!! Warning "Backtesting"
|
||||
|
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
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
|
||||
|
||||
Plotting modules use the Plotly library. You can install / upgrade this by running the following command:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
markdown==3.6
|
||||
mkdocs==1.6.0
|
||||
mkdocs-material==9.5.24
|
||||
mkdocs-material==9.5.27
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==10.8.1
|
||||
jinja2==3.1.4
|
||||
|
|
|
@ -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]
|
||||
```
|
||||
|
||||
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"
|
||||
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:
|
||||
|
|
|
@ -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).
|
||||
|
||||
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)).
|
||||
|
||||
|
@ -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"
|
||||
|
||||
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.
|
||||
|
||||
``` python
|
||||
|
@ -492,7 +494,8 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
|
|||
candle = dataframe.iloc[-1].squeeze()
|
||||
side = 1 if trade.is_short else -1
|
||||
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)
|
||||
|
||||
```
|
||||
|
|
|
@ -13,28 +13,28 @@ The following attributes / properties are available for each individual trade -
|
|||
|
||||
| Attribute | DataType | Description |
|
||||
|------------|-------------|-------------|
|
||||
`pair`| string | Pair of this trade
|
||||
`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)
|
||||
`close_rate`| float | Close rate - only set when is_open = False
|
||||
`stake_amount`| float | Amount in Stake (or Quote) currency.
|
||||
`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_utc`| datetime | Timestamp when trade was opened - in UTC
|
||||
`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_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.
|
||||
`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
|
||||
`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)
|
||||
`date_last_filled_utc` | datetime | Time of the last filled order
|
||||
`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.
|
||||
`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_exits` | int | Number of successful (filled) exit orders
|
||||
| `pair` | string | Pair of this trade. |
|
||||
| `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). |
|
||||
| `close_rate` | float | Close rate - only set when is_open = False. |
|
||||
| `stake_amount` | float | Amount in Stake (or Quote) currency. |
|
||||
| `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_utc` | datetime | Timestamp when trade was opened - in UTC. |
|
||||
| `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_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. |
|
||||
| `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. |
|
||||
| `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). |
|
||||
| `date_last_filled_utc` | datetime | Time of the last filled order. |
|
||||
| `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. |
|
||||
| `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_exits` | int | Number of successful (filled) exit orders. |
|
||||
|
||||
## Class methods
|
||||
|
||||
|
|
|
@ -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.
|
||||
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
|
||||
|
||||
!!! Note "64bit Python version"
|
||||
|
@ -14,13 +38,7 @@ Otherwise, please follow the instructions below.
|
|||
!!! 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.
|
||||
|
||||
### 1. Clone the git repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/freqtrade/freqtrade.git
|
||||
```
|
||||
|
||||
### 2. Install ta-lib
|
||||
### Install ta-lib
|
||||
|
||||
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2024.5"
|
||||
__version__ = "2024.6"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
@ -30,5 +30,5 @@ if "dev" in __version__:
|
|||
versionfile = Path("./freqtrade_commit")
|
||||
if versionfile.is_file():
|
||||
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
||||
except Exception:
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
|
|
@ -187,7 +187,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||
"Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||
"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"],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
This module contains the configuration class
|
||||
"""
|
||||
|
||||
import ast
|
||||
import logging
|
||||
import warnings
|
||||
from copy import deepcopy
|
||||
|
@ -301,7 +302,7 @@ class Configuration:
|
|||
|
||||
# Edge section:
|
||||
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_max": txt_range[1]})
|
||||
config["edge"].update({"stoploss_range_step": txt_range[2]})
|
||||
|
|
|
@ -618,6 +618,11 @@ def download_data_main(config: Config) -> None:
|
|||
# Start downloading
|
||||
try:
|
||||
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(
|
||||
exchange,
|
||||
pairs=expanded_pairs,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from enum import Enum
|
||||
|
||||
|
||||
class RunMode(Enum):
|
||||
class RunMode(str, Enum):
|
||||
"""
|
||||
Bot running mode (backtest, hyperopt, ...)
|
||||
can be "live", "dry-run", "backtest", "edge", "hyperopt".
|
||||
|
|
|
@ -28,6 +28,7 @@ class Binance(Exchange):
|
|||
"ohlcv_candle_limit": 1000,
|
||||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "fromId",
|
||||
"trades_has_history": True,
|
||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -20,4 +20,5 @@ class Bingx(Exchange):
|
|||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"order_time_in_force": ["GTC", "IOC", "PO"],
|
||||
"trades_has_history": False, # Endpoint doesn't seem to support pagination
|
||||
}
|
||||
|
|
|
@ -18,4 +18,5 @@ class Bitmart(Exchange):
|
|||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": False, # Bitmart API does not support stoploss orders
|
||||
"ohlcv_candle_limit": 200,
|
||||
"trades_has_history": False, # Endpoint doesn't seem to support pagination
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ class Bybit(Exchange):
|
|||
"ohlcv_candle_limit": 1000,
|
||||
"ohlcv_has_history": True,
|
||||
"order_time_in_force": ["GTC", "FOK", "IOC", "PO"],
|
||||
"trades_has_history": False, # Endpoint doesn't support pagination
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"ohlcv_has_history": True,
|
||||
|
|
|
@ -117,6 +117,7 @@ class Exchange:
|
|||
"tickers_have_price": True,
|
||||
"trades_pagination": "time", # Possible are "time" or "id"
|
||||
"trades_pagination_arg": "since",
|
||||
"trades_has_history": False,
|
||||
"l2_limit_range": None,
|
||||
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
|
||||
"mark_ohlcv_price": "mark",
|
||||
|
@ -151,7 +152,7 @@ class Exchange:
|
|||
:return: None
|
||||
"""
|
||||
self._api: ccxt.Exchange
|
||||
self._api_async: ccxt_async.Exchange = None
|
||||
self._api_async: ccxt_async.Exchange
|
||||
self._markets: Dict = {}
|
||||
self._trading_fees: Dict[str, Any] = {}
|
||||
self._leverage_tiers: Dict[str, List[Dict]] = {}
|
||||
|
@ -233,7 +234,7 @@ class Exchange:
|
|||
self.required_candle_call_count = 1
|
||||
if validate:
|
||||
# Initial markets load
|
||||
self._load_markets()
|
||||
self.reload_markets(True, load_leverage_tiers=False)
|
||||
self.validate_config(config)
|
||||
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
|
@ -258,7 +259,7 @@ class Exchange:
|
|||
def close(self):
|
||||
logger.debug("Exchange object destroyed, closing async loop")
|
||||
if (
|
||||
self._api_async
|
||||
getattr(self, "_api_async", None)
|
||||
and inspect.iscoroutinefunction(self._api_async.close)
|
||||
and self._api_async.session
|
||||
):
|
||||
|
@ -354,7 +355,7 @@ class Exchange:
|
|||
"""exchange ccxt markets"""
|
||||
if not self._markets:
|
||||
logger.info("Markets were not loaded. Loading them now..")
|
||||
self._load_markets()
|
||||
self.reload_markets(True)
|
||||
return self._markets
|
||||
|
||||
@property
|
||||
|
@ -530,30 +531,26 @@ class Exchange:
|
|||
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:
|
||||
if self._api_async:
|
||||
self.loop.run_until_complete(self._api_async.load_markets(reload=reload, params={}))
|
||||
markets = self.loop.run_until_complete(
|
||||
self._api_async.load_markets(reload=reload, params={})
|
||||
)
|
||||
|
||||
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
||||
logger.warning("Could not load async markets. Reason: %s", e)
|
||||
return
|
||||
if isinstance(markets, Exception):
|
||||
raise markets
|
||||
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:
|
||||
"""Initialize markets both sync and async"""
|
||||
try:
|
||||
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()
|
||||
def reload_markets(self, force: bool = False, *, load_leverage_tiers: bool = True) -> None:
|
||||
"""
|
||||
Reload / Initialize markets both sync and async if refresh interval has passed
|
||||
|
||||
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
|
||||
is_initial = self._last_markets_refresh == 0
|
||||
if (
|
||||
not force
|
||||
and self._last_markets_refresh > 0
|
||||
|
@ -562,13 +559,18 @@ class Exchange:
|
|||
return None
|
||||
logger.debug("Performing scheduled market reload..")
|
||||
try:
|
||||
self._markets = self._api.load_markets(reload=True, params={})
|
||||
# Also reload async markets to avoid issues with newly listed pairs
|
||||
self._load_async_markets(reload=True)
|
||||
# Reload async markets, then assign them to sync api
|
||||
self._markets = 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.fill_leverage_tiers()
|
||||
except ccxt.BaseError:
|
||||
logger.exception("Could not reload markets.")
|
||||
|
||||
if is_initial and self._ft_has["needs_trading_fees"]:
|
||||
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:
|
||||
"""
|
||||
|
|
|
@ -31,6 +31,7 @@ class Gate(Exchange):
|
|||
"stop_price_param": "stopPrice",
|
||||
"stop_price_prop": "stopPrice",
|
||||
"marketOrderRequiresPrice": True,
|
||||
"trades_has_history": False, # Endpoint would support this - but ccxt doesn't.
|
||||
}
|
||||
|
||||
_ft_has_futures: Dict = {
|
||||
|
|
|
@ -28,6 +28,7 @@ class Htx(Exchange):
|
|||
"1w": 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:
|
||||
|
|
|
@ -31,6 +31,7 @@ class Kraken(Exchange):
|
|||
"trades_pagination": "id",
|
||||
"trades_pagination_arg": "since",
|
||||
"trades_pagination_overlap": False,
|
||||
"trades_has_history": True,
|
||||
"mark_ohlcv_timeframe": "4h",
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ class Okx(Exchange):
|
|||
"funding_fee_timeframe": "8h",
|
||||
"stoploss_order_types": {"limit": "limit"},
|
||||
"stoploss_on_exchange": True,
|
||||
"trades_has_history": False, # Endpoint doesn't have a "since" parameter
|
||||
}
|
||||
_ft_has_futures: Dict = {
|
||||
"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,
|
||||
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_epochs = max(self.n_steps // n_batches, 1)
|
||||
if n_epochs <= 10:
|
||||
|
|
|
@ -217,7 +217,7 @@ class FreqtradeBot(LoggingMixin):
|
|||
except Exception:
|
||||
# Exceptions here will be happening if the db disappeared.
|
||||
# At which point we can no longer commit anyway.
|
||||
pass
|
||||
logger.exception("Error during cleanup")
|
||||
|
||||
def startup(self) -> None:
|
||||
"""
|
||||
|
|
|
@ -13,7 +13,7 @@ def get_strategy_run_id(strategy) -> str:
|
|||
:param strategy: strategy object.
|
||||
:return: hex string id.
|
||||
"""
|
||||
digest = hashlib.sha1()
|
||||
digest = hashlib.sha1() # noqa: S324
|
||||
config = deepcopy(strategy.config)
|
||||
|
||||
# Options that have no impact on results of individual backtest.
|
||||
|
|
|
@ -489,7 +489,7 @@ class Hyperopt:
|
|||
)
|
||||
|
||||
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]:
|
||||
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from tabulate import tabulate
|
||||
|
||||
|
@ -20,13 +20,13 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
|||
|
||||
|
||||
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]:
|
||||
"""
|
||||
Generate header lines (goes in line with _generate_result_line())
|
||||
"""
|
||||
return [
|
||||
first_column,
|
||||
*([first_column] if isinstance(first_column, str) else first_column),
|
||||
direction,
|
||||
"Avg Profit %",
|
||||
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
|
||||
"""
|
||||
|
||||
headers = _get_line_header("Pair", stake_currency)
|
||||
headers = _get_line_header("Pair", stake_currency, "Trades")
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
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
|
||||
:return: pretty printed table with tabulate as string
|
||||
"""
|
||||
floatfmt = _get_line_floatfmt(stake_currency)
|
||||
fallback: str = ""
|
||||
is_list = False
|
||||
if tag_type == "enter_tag":
|
||||
headers = _get_line_header("TAG", stake_currency)
|
||||
else:
|
||||
headers = _get_line_header("Enter Tag", stake_currency, "Entries")
|
||||
elif tag_type == "exit_tag":
|
||||
headers = _get_line_header("Exit Reason", stake_currency, "Exits")
|
||||
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 = [
|
||||
[
|
||||
(
|
||||
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
|
||||
else t.get(fallback, "OTHER")
|
||||
else [t.get(fallback, "OTHER")]
|
||||
),
|
||||
t["trades"],
|
||||
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
|
||||
"""
|
||||
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
|
||||
# therefore we slip this column in only for strategy summary here.
|
||||
headers.append("Drawdown")
|
||||
|
@ -380,6 +390,32 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||
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(
|
||||
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(table)
|
||||
|
||||
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)
|
||||
_show_tag_subresults(results, stake_currency)
|
||||
|
||||
for period in backtest_breakdown:
|
||||
if period in results.get("periodic_breakdown", {}):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from copy import deepcopy
|
||||
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
|
||||
from pandas import DataFrame, Series, concat, to_datetime
|
||||
|
@ -68,7 +68,9 @@ def generate_rejected_signals(
|
|||
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.
|
||||
"""
|
||||
|
@ -141,7 +143,10 @@ def generate_pair_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]:
|
||||
"""
|
||||
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 = []
|
||||
|
||||
if tag_type in results.columns:
|
||||
for tag, count in results[tag_type].value_counts().items():
|
||||
result = results[results[tag_type] == tag]
|
||||
if skip_nan and result["profit_abs"].isnull().all():
|
||||
if all(
|
||||
tag in results.columns for tag in (tag_type if isinstance(tag_type, list) else [tag_type])
|
||||
):
|
||||
for tags, group in results.groupby(tag_type):
|
||||
if skip_nan and group["profit_abs"].isnull().all():
|
||||
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 %:
|
||||
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,
|
||||
)
|
||||
|
||||
enter_tag_results = generate_tag_metrics(
|
||||
enter_tag_stats = generate_tag_metrics(
|
||||
"enter_tag", starting_balance=start_balance, results=results, skip_nan=False
|
||||
)
|
||||
exit_reason_stats = generate_tag_metrics(
|
||||
"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(
|
||||
pairlist,
|
||||
stake_currency=stake_currency,
|
||||
|
@ -425,8 +437,9 @@ def generate_strategy_stats(
|
|||
"best_pair": best_pair,
|
||||
"worst_pair": worst_pair,
|
||||
"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,
|
||||
"mix_tag_stats": mix_tag_stats,
|
||||
"left_open_trades": left_open_results,
|
||||
"total_trades": len(results),
|
||||
"trade_count_long": len(results.loc[~results["is_short"]]),
|
||||
|
|
|
@ -5,11 +5,11 @@ Minimum age (days listed) pair list filter
|
|||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
|
@ -21,24 +21,17 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class AgeFilter(IPairList):
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||
self._symbolsChecked: Dict[str, int] = {}
|
||||
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
||||
|
||||
self._min_days_listed = pairlistconfig.get("min_days_listed", 10)
|
||||
self._max_days_listed = pairlistconfig.get("max_days_listed")
|
||||
self._min_days_listed = self._pairlistconfig.get("min_days_listed", 10)
|
||||
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:
|
||||
raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
|
||||
if self._min_days_listed > candle_limit:
|
||||
|
|
|
@ -3,9 +3,8 @@ Full trade slots pair list filter
|
|||
"""
|
||||
|
||||
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.persistence import Trade
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
|
@ -15,16 +14,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
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
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
|
|
|
@ -3,7 +3,7 @@ PairList Handler base class
|
|||
"""
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod, abstractproperty
|
||||
from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
||||
|
||||
|
@ -87,7 +87,8 @@ class IPairList(LoggingMixin, ABC):
|
|||
"""
|
||||
return self.__class__.__name__
|
||||
|
||||
@abstractproperty
|
||||
@property
|
||||
@abstractmethod
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
Boolean property defining if tickers are necessary.
|
||||
|
|
|
@ -5,11 +5,10 @@ Provides dynamic pair list based on Market Cap
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from typing import Dict, List
|
||||
|
||||
from cachetools import TTLCache
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
@ -22,15 +21,8 @@ logger = logging.getLogger(__name__)
|
|||
class MarketCapPairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if "number_assets" not in self._pairlistconfig:
|
||||
raise OperationalException(
|
||||
|
@ -38,14 +30,14 @@ class MarketCapPairList(IPairList):
|
|||
'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._max_rank = self._pairlistconfig.get("max_rank", 30)
|
||||
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
|
||||
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
|
||||
self._def_candletype = self._config["candle_type_def"]
|
||||
|
||||
_coingecko_config = config.get("coingecko", {})
|
||||
_coingecko_config = self._config.get("coingecko", {})
|
||||
|
||||
self._coingecko: FtCoinGeckoApi = FtCoinGeckoApi(
|
||||
api_key=_coingecko_config.get("api_key", ""),
|
||||
|
|
|
@ -3,9 +3,8 @@ Offset pair list filter
|
|||
"""
|
||||
|
||||
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.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
@ -15,18 +14,11 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class OffsetFilter(IPairList):
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._offset = pairlistconfig.get("offset", 0)
|
||||
self._number_pairs = pairlistconfig.get("number_assets", 0)
|
||||
self._offset = self._pairlistconfig.get("offset", 0)
|
||||
self._number_pairs = self._pairlistconfig.get("number_assets", 0)
|
||||
|
||||
if self._offset < 0:
|
||||
raise OperationalException("OffsetFilter requires offset to be >= 0")
|
||||
|
|
|
@ -3,11 +3,10 @@ Performance pair list filter
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from typing import Dict, List
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
@ -17,18 +16,11 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class PerformanceFilter(IPairList):
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._minutes = pairlistconfig.get("minutes", 0)
|
||||
self._min_profit = pairlistconfig.get("min_profit")
|
||||
self._minutes = self._pairlistconfig.get("minutes", 0)
|
||||
self._min_profit = self._pairlistconfig.get("min_profit")
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
|
|
|
@ -3,9 +3,8 @@ Precision pair list filter
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Optional
|
||||
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import ROUND_UP
|
||||
from freqtrade.exchange.types import Ticker
|
||||
|
@ -16,15 +15,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class PrecisionFilter(IPairList):
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if "stoploss" not in self._config:
|
||||
raise OperationalException(
|
||||
|
|
|
@ -3,9 +3,8 @@ Price pair list filter
|
|||
"""
|
||||
|
||||
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.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
@ -15,26 +14,19 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class PriceFilter(IPairList):
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
raise OperationalException("PriceFilter requires max_value to be >= 0")
|
||||
self._enabled = (
|
||||
|
|
|
@ -5,7 +5,7 @@ Provides pair list from Leader data
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
|
@ -32,19 +32,12 @@ class ProducerPairList(IPairList):
|
|||
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Dict[str, Any],
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._num_assets: int = self._pairlistconfig.get("number_assets", 0)
|
||||
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(
|
||||
"ProducerPairList requires external_message_consumer to be enabled."
|
||||
)
|
||||
|
|
|
@ -14,7 +14,6 @@ from cachetools import TTLCache
|
|||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.configuration.load_config import CONFIG_PARSE_MODE
|
||||
from freqtrade.constants import Config
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
@ -27,15 +26,8 @@ logger = logging.getLogger(__name__)
|
|||
class RemotePairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if "number_assets" not in self._pairlistconfig:
|
||||
raise OperationalException(
|
||||
|
|
|
@ -4,9 +4,8 @@ Shuffle pair list filter
|
|||
|
||||
import logging
|
||||
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.exchange import timeframe_to_seconds
|
||||
from freqtrade.exchange.types import Tickers
|
||||
|
@ -20,27 +19,20 @@ ShuffleValues = Literal["candle", "iteration"]
|
|||
|
||||
|
||||
class ShuffleFilter(IPairList):
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# 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.
|
||||
if config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN):
|
||||
if self._config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN):
|
||||
self._seed = None
|
||||
logger.info("Live mode detected, not applying seed.")
|
||||
else:
|
||||
self._seed = pairlistconfig.get("seed")
|
||||
self._seed = self._pairlistconfig.get("seed")
|
||||
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
|
||||
|
||||
self._random = random.Random(self._seed)
|
||||
self._shuffle_freq: ShuffleValues = pairlistconfig.get("shuffle_frequency", "candle")
|
||||
self._random = random.Random(self._seed) # noqa: S311
|
||||
self._shuffle_freq: ShuffleValues = self._pairlistconfig.get("shuffle_frequency", "candle")
|
||||
self.__pairlist_cache = PeriodicCache(
|
||||
maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"])
|
||||
)
|
||||
|
|
|
@ -3,9 +3,8 @@ Spread pair list filter
|
|||
"""
|
||||
|
||||
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.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
@ -15,17 +14,10 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class SpreadFilter(IPairList):
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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
|
||||
|
||||
if not self._exchange.get_option("tickers_have_bid_ask"):
|
||||
|
|
|
@ -6,9 +6,8 @@ Provides pair white list as it configured in config
|
|||
|
||||
import logging
|
||||
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.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
|
||||
|
@ -19,15 +18,8 @@ logger = logging.getLogger(__name__)
|
|||
class StaticPairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._allow_inactive = self._pairlistconfig.get("allow_inactive", False)
|
||||
|
||||
|
|
|
@ -5,13 +5,13 @@ Volatility pairlist filter
|
|||
import logging
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
from cachetools import TTLCache
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
|
@ -27,26 +27,19 @@ class VolatilityFilter(IPairList):
|
|||
Filters pairs by volatility
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._days = pairlistconfig.get("lookback_days", 10)
|
||||
self._min_volatility = pairlistconfig.get("min_volatility", 0)
|
||||
self._max_volatility = pairlistconfig.get("max_volatility", sys.maxsize)
|
||||
self._refresh_period = pairlistconfig.get("refresh_period", 1440)
|
||||
self._days = self._pairlistconfig.get("lookback_days", 10)
|
||||
self._min_volatility = self._pairlistconfig.get("min_volatility", 0)
|
||||
self._max_volatility = self._pairlistconfig.get("max_volatility", sys.maxsize)
|
||||
self._refresh_period = self._pairlistconfig.get("refresh_period", 1440)
|
||||
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)
|
||||
|
||||
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:
|
||||
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
|
||||
if self._days > candle_limit:
|
||||
|
|
|
@ -10,7 +10,7 @@ from typing import Any, Dict, List, Literal
|
|||
|
||||
from cachetools import TTLCache
|
||||
|
||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||
from freqtrade.exchange.types import Tickers
|
||||
|
@ -27,15 +27,8 @@ SORT_VALUES = ["quoteVolume"]
|
|||
class VolumePairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if "number_assets" not in self._pairlistconfig:
|
||||
raise OperationalException(
|
||||
|
@ -43,7 +36,7 @@ class VolumePairList(IPairList):
|
|||
'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._sort_key: Literal["quoteVolume"] = self._pairlistconfig.get("sort_key", "quoteVolume")
|
||||
self._min_value = self._pairlistconfig.get("min_value", 0)
|
||||
|
@ -94,7 +87,7 @@ class VolumePairList(IPairList):
|
|||
if not self._validate_keys(self._sort_key):
|
||||
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"]
|
||||
)
|
||||
if self._lookback_period < 0:
|
||||
|
|
|
@ -4,12 +4,12 @@ Rate of change pairlist filter
|
|||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from cachetools import TTLCache
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
|
@ -21,26 +21,19 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class RangeStabilityFilter(IPairList):
|
||||
def __init__(
|
||||
self,
|
||||
exchange,
|
||||
pairlistmanager,
|
||||
config: Config,
|
||||
pairlistconfig: Dict[str, Any],
|
||||
pairlist_pos: int,
|
||||
) -> None:
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._days = pairlistconfig.get("lookback_days", 10)
|
||||
self._min_rate_of_change = pairlistconfig.get("min_rate_of_change", 0.01)
|
||||
self._max_rate_of_change = pairlistconfig.get("max_rate_of_change")
|
||||
self._refresh_period = pairlistconfig.get("refresh_period", 86400)
|
||||
self._days = self._pairlistconfig.get("lookback_days", 10)
|
||||
self._min_rate_of_change = self._pairlistconfig.get("min_rate_of_change", 0.01)
|
||||
self._max_rate_of_change = self._pairlistconfig.get("max_rate_of_change")
|
||||
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
|
||||
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)
|
||||
|
||||
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:
|
||||
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
|
||||
if self._days > candle_limit:
|
||||
|
|
|
@ -31,7 +31,7 @@ security = HTTPBasic()
|
|||
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(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
|
@ -86,11 +86,11 @@ async def validate_ws_token(
|
|||
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()
|
||||
if token_type == "access":
|
||||
if token_type == "access": # noqa: S105
|
||||
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)
|
||||
else:
|
||||
raise ValueError()
|
||||
|
@ -127,9 +127,15 @@ def token_login(
|
|||
):
|
||||
if verify_auth(api_config, form_data.username, form_data.password):
|
||||
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(
|
||||
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 {
|
||||
"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")
|
||||
token_data = {"identity": {"u": u}}
|
||||
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}
|
||||
|
|
|
@ -1466,6 +1466,8 @@ class RPC:
|
|||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
|
||||
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:
|
||||
strategy.plot_config["subplots"] = {}
|
||||
|
|
|
@ -1787,7 +1787,7 @@ class Telegram(RPCHandler):
|
|||
"_Bot Control_\n"
|
||||
"------------\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"
|
||||
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
|
||||
"regardless of profit`\n"
|
||||
|
@ -1820,7 +1820,7 @@ class Telegram(RPCHandler):
|
|||
"that represents the current market direction. If no direction is provided `"
|
||||
"`the currently set market direction will be output.` \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"
|
||||
"------------\n"
|
||||
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
|
||||
|
|
|
@ -6,6 +6,7 @@ This module defines the interface to apply for strategies
|
|||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import isinf, isnan
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
from pandas import DataFrame
|
||||
|
@ -1423,7 +1424,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||
after_fill=after_fill,
|
||||
)
|
||||
# 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
|
||||
trade.adjust_stop_loss(
|
||||
bound or current_rate, stop_loss_value, allow_refresh=after_fill
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from freqtrade_client.ft_rest_client import FtRestClient
|
||||
|
||||
|
||||
__version__ = "2024.5"
|
||||
__version__ = "2024.6"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
@ -31,7 +31,7 @@ if "dev" in __version__:
|
|||
versionfile = Path("./freqtrade_commit")
|
||||
if versionfile.is_file():
|
||||
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
||||
except Exception:
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
|
||||
__all__ = ["FtRestClient"]
|
||||
|
|
|
@ -81,12 +81,12 @@ def print_commands():
|
|||
print(f"{x}\n\t{doc}\n")
|
||||
|
||||
|
||||
def main_exec(args: Dict[str, Any]):
|
||||
if args.get("show"):
|
||||
def main_exec(parsed: Dict[str, Any]):
|
||||
if parsed.get("show"):
|
||||
print_commands()
|
||||
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")
|
||||
port = config.get("api_server", {}).get("listen_port", "8080")
|
||||
username = config.get("api_server", {}).get("username")
|
||||
|
@ -96,13 +96,24 @@ def main_exec(args: Dict[str, Any]):
|
|||
client = FtRestClient(server_url, username, password)
|
||||
|
||||
m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")]
|
||||
command = args["command"]
|
||||
command = parsed["command"]
|
||||
if command not in m:
|
||||
logger.error(f"Command {command} not defined")
|
||||
print_commands()
|
||||
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():
|
||||
|
|
|
@ -54,7 +54,7 @@ class FtRestClient:
|
|||
# return resp.text
|
||||
return resp.json()
|
||||
except ConnectionError:
|
||||
logger.warning("Connection error")
|
||||
logger.warning(f"Connection error - could not connect to {netloc}.")
|
||||
|
||||
def _get(self, apipath, params: ParamsT = None):
|
||||
return self._call("GET", apipath, params=params)
|
||||
|
@ -312,20 +312,48 @@ class FtRestClient:
|
|||
data = {"pair": pair, "price": price}
|
||||
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
|
||||
|
||||
:param pair: Pair to buy (ETH/BTC)
|
||||
:param side: 'long' or 'short'
|
||||
: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
|
||||
"""
|
||||
data = {
|
||||
"pair": pair,
|
||||
"side": side,
|
||||
}
|
||||
|
||||
if 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)
|
||||
|
||||
def forceexit(self, tradeid, ordertype=None, amount=None):
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Requirements for freqtrade client library
|
||||
requests==2.32.2
|
||||
requests==2.32.3
|
||||
python-rapidjson==1.17
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import re
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import ANY, MagicMock
|
||||
|
||||
import pytest
|
||||
from requests.exceptions import ConnectionError
|
||||
|
@ -52,70 +52,89 @@ def test_FtRestClient_call_invalid(caplog):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"method,args",
|
||||
"method,args,kwargs",
|
||||
[
|
||||
("start", []),
|
||||
("stop", []),
|
||||
("stopbuy", []),
|
||||
("reload_config", []),
|
||||
("balance", []),
|
||||
("count", []),
|
||||
("entries", []),
|
||||
("exits", []),
|
||||
("mix_tags", []),
|
||||
("locks", []),
|
||||
("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"]),
|
||||
("delete_lock", [2]),
|
||||
("daily", []),
|
||||
("daily", [15]),
|
||||
("weekly", []),
|
||||
("weekly", [15]),
|
||||
("monthly", []),
|
||||
("monthly", [12]),
|
||||
("edge", []),
|
||||
("profit", []),
|
||||
("stats", []),
|
||||
("performance", []),
|
||||
("status", []),
|
||||
("version", []),
|
||||
("show_config", []),
|
||||
("ping", []),
|
||||
("logs", []),
|
||||
("logs", [55]),
|
||||
("trades", []),
|
||||
("trades", [5]),
|
||||
("trades", [5, 5]), # With offset
|
||||
("trade", [1]),
|
||||
("delete_trade", [1]),
|
||||
("cancel_open_order", [1]),
|
||||
("whitelist", []),
|
||||
("blacklist", []),
|
||||
("blacklist", ["XRP/USDT"]),
|
||||
("blacklist", ["XRP/USDT", "BTC/USDT"]),
|
||||
("forcebuy", ["XRP/USDT"]),
|
||||
("forcebuy", ["XRP/USDT", 1.5]),
|
||||
("forceenter", ["XRP/USDT", "short"]),
|
||||
("forceenter", ["XRP/USDT", "short", 1.5]),
|
||||
("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_history", ["XRP/USDT", "5m", "SampleStrategy"]),
|
||||
("sysinfo", []),
|
||||
("health", []),
|
||||
("start", [], {}),
|
||||
("stop", [], {}),
|
||||
("stopbuy", [], {}),
|
||||
("reload_config", [], {}),
|
||||
("balance", [], {}),
|
||||
("count", [], {}),
|
||||
("entries", [], {}),
|
||||
("exits", [], {}),
|
||||
("mix_tags", [], {}),
|
||||
("locks", [], {}),
|
||||
("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"], {}),
|
||||
("delete_lock", [2], {}),
|
||||
("daily", [], {}),
|
||||
("daily", [15], {}),
|
||||
("weekly", [], {}),
|
||||
("weekly", [15], {}),
|
||||
("monthly", [], {}),
|
||||
("monthly", [12], {}),
|
||||
("edge", [], {}),
|
||||
("profit", [], {}),
|
||||
("stats", [], {}),
|
||||
("performance", [], {}),
|
||||
("status", [], {}),
|
||||
("version", [], {}),
|
||||
("show_config", [], {}),
|
||||
("ping", [], {}),
|
||||
("logs", [], {}),
|
||||
("logs", [55], {}),
|
||||
("trades", [], {}),
|
||||
("trades", [5], {}),
|
||||
("trades", [5, 5], {}), # With offset
|
||||
("trade", [1], {}),
|
||||
("delete_trade", [1], {}),
|
||||
("cancel_open_order", [1], {}),
|
||||
("whitelist", [], {}),
|
||||
("blacklist", [], {}),
|
||||
("blacklist", ["XRP/USDT"], {}),
|
||||
("blacklist", ["XRP/USDT", "BTC/USDT"], {}),
|
||||
("forcebuy", ["XRP/USDT"], {}),
|
||||
("forcebuy", ["XRP/USDT", 1.5], {}),
|
||||
("forceenter", ["XRP/USDT", "short"], {}),
|
||||
("forceenter", ["XRP/USDT", "short", 1.5], {}),
|
||||
("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market"}),
|
||||
("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market", "stake_amount": 100}),
|
||||
(
|
||||
"forceenter",
|
||||
["XRP/USDT", "short", 1.5],
|
||||
{"order_type": "market", "stake_amount": 100, "leverage": 10.0},
|
||||
),
|
||||
(
|
||||
"forceenter",
|
||||
["XRP/USDT", "short", 1.5],
|
||||
{
|
||||
"order_type": "market",
|
||||
"stake_amount": 100,
|
||||
"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()
|
||||
exec = getattr(client, method)
|
||||
exec(*args)
|
||||
exec(*args, **kwargs)
|
||||
assert mock.call_count == 1
|
||||
|
||||
|
||||
|
@ -148,3 +167,40 @@ def test_ft_client(mocker, capsys, caplog):
|
|||
)
|
||||
main_exec(args)
|
||||
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,5 +1,6 @@
|
|||
site_name: Freqtrade
|
||||
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
|
||||
edit_uri: edit/develop/docs/
|
||||
use_directory_urls: True
|
||||
|
|
|
@ -82,6 +82,9 @@ skip_glob = ["**/.env*", "**/env/*", "**/.venv/*", "**/docs/*", "**/user_data/*"
|
|||
known_first_party = ["freqtrade_client"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_format = "%(asctime)s %(levelname)s %(message)s"
|
||||
log_date_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
asyncio_mode = "auto"
|
||||
addopts = "--dist loadscope"
|
||||
|
||||
|
@ -135,7 +138,7 @@ extend-select = [
|
|||
# "EXE", # flake8-executable
|
||||
# "C4", # flake8-comprehensions
|
||||
"YTT", # flake8-2020
|
||||
# "S", # flake8-bandit
|
||||
"S", # flake8-bandit
|
||||
# "DTZ", # flake8-datetimez
|
||||
# "RSE", # flake8-raise
|
||||
# "TCH", # flake8-type-checking
|
||||
|
@ -148,13 +151,30 @@ extend-ignore = [
|
|||
"E272", # Multiple spaces before keyword
|
||||
"E221", # Multiple spaces before operator
|
||||
"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]
|
||||
max-complexity = 12
|
||||
|
||||
[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]
|
||||
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==4.0.1
|
||||
ruff==0.4.5
|
||||
ruff==0.4.10
|
||||
mypy==1.10.0
|
||||
pre-commit==3.7.1
|
||||
pytest==8.2.1
|
||||
pytest==8.2.2
|
||||
pytest-asyncio==0.23.7
|
||||
pytest-cov==5.0.0
|
||||
pytest-mock==3.14.0
|
||||
|
@ -26,6 +26,6 @@ nbconvert==7.16.4
|
|||
# mypy types
|
||||
types-cachetools==5.3.0.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-python-dateutil==2.9.0.20240316
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
-r requirements-freqai.txt
|
||||
|
||||
# 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
|
||||
stable_baselines3==2.3.2
|
||||
sb3_contrib>=2.2.1
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
scikit-learn==1.5.0
|
||||
joblib==1.4.2
|
||||
catboost==1.2.5; 'arm' not in platform_machine
|
||||
lightgbm==4.3.0
|
||||
lightgbm==4.4.0
|
||||
xgboost==2.0.3
|
||||
tensorboard==2.16.2
|
||||
tensorboard==2.17.0
|
||||
datasieve==0.1.7
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
scipy==1.13.1
|
||||
scikit-learn==1.5.0
|
||||
ft-scikit-optimize==0.9.2
|
||||
filelock==3.14.0
|
||||
filelock==3.15.4
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
numpy==1.26.4
|
||||
pandas==2.2.2
|
||||
bottleneck==1.4.0
|
||||
numexpr==2.10.1
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==4.3.35
|
||||
cryptography==42.0.7
|
||||
ccxt==4.3.50
|
||||
cryptography==42.0.8
|
||||
aiohttp==3.9.5
|
||||
SQLAlchemy==2.0.30
|
||||
python-telegram-bot==21.2
|
||||
SQLAlchemy==2.0.31
|
||||
python-telegram-bot==21.3
|
||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||
httpx>=0.24.1
|
||||
humanize==4.9.0
|
||||
cachetools==5.3.3
|
||||
requests==2.32.2
|
||||
urllib3==2.2.1
|
||||
requests==2.32.3
|
||||
urllib3==2.2.2
|
||||
jsonschema==4.22.0
|
||||
TA-Lib==0.4.29
|
||||
TA-Lib==0.4.31
|
||||
technical==1.4.3
|
||||
tabulate==0.9.0
|
||||
pycoingecko==3.1.0
|
||||
|
@ -30,18 +32,18 @@ py_find_1st==1.1.6
|
|||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.17
|
||||
# Properly format api responses
|
||||
orjson==3.10.3
|
||||
orjson==3.10.5
|
||||
|
||||
# Notify systemd
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.111.0
|
||||
pydantic==2.7.1
|
||||
uvicorn==0.29.0
|
||||
pydantic==2.7.4
|
||||
uvicorn==0.30.1
|
||||
pyjwt==2.8.0
|
||||
aiofiles==23.2.1
|
||||
psutil==5.9.8
|
||||
psutil==6.0.0
|
||||
|
||||
# Support for colorized terminal output
|
||||
colorama==0.4.6
|
||||
|
@ -60,4 +62,4 @@ websockets==12.0
|
|||
janus==1.0.0
|
||||
|
||||
ast-comments==1.2.2
|
||||
packaging==24.0
|
||||
packaging==24.1
|
||||
|
|
285
setup.ps1
Normal file
285
setup.ps1
Normal 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
|
2
setup.sh
2
setup.sh
|
@ -49,7 +49,7 @@ function updateenv() {
|
|||
source .venv/bin/activate
|
||||
SYS_ARCH=$(uname -m)
|
||||
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_PLOT=""
|
||||
REQUIREMENTS_FREQAI=""
|
||||
|
|
|
@ -238,7 +238,6 @@ def patched_configuration_load_config_file(mocker, config) -> None:
|
|||
def patch_exchange(
|
||||
mocker, api_mock=None, id="binance", mock_markets=True, mock_supported_modes=True
|
||||
) -> None:
|
||||
mocker.patch(f"{EXMS}._load_async_markets", return_value={})
|
||||
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
||||
mocker.patch(f"{EXMS}.validate_timeframes", MagicMock())
|
||||
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")
|
||||
|
||||
if mock_markets:
|
||||
mocker.patch(f"{EXMS}._load_async_markets", return_value={})
|
||||
if isinstance(mock_markets, bool):
|
||||
mock_markets = get_markets()
|
||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=mock_markets))
|
||||
|
|
|
@ -83,6 +83,12 @@ def test_download_data_main_trades(mocker):
|
|||
assert dl_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):
|
||||
patch_exchange(mocker, id="kraken")
|
||||
|
|
|
@ -67,10 +67,10 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
|
|||
"enter_tag_long_b",
|
||||
],
|
||||
"exit_reason": [
|
||||
ExitType.ROI,
|
||||
ExitType.EXIT_SIGNAL,
|
||||
ExitType.STOP_LOSS,
|
||||
ExitType.TRAILING_STOP_LOSS,
|
||||
ExitType.ROI.value,
|
||||
ExitType.EXIT_SIGNAL.value,
|
||||
ExitType.STOP_LOSS.value,
|
||||
ExitType.TRAILING_STOP_LOSS.value,
|
||||
],
|
||||
}
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
|||
|
||||
import ccxt
|
||||
import pytest
|
||||
from numpy import NaN
|
||||
from numpy import nan
|
||||
from pandas import DataFrame
|
||||
|
||||
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):
|
||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
||||
mocker.patch(f"{EXMS}.reload_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
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}.validate_pairs")
|
||||
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_pricing")
|
||||
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
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
|
||||
exchange._load_async_markets()
|
||||
|
||||
assert log_has("Could not load async markets. Reason: deadbeef", caplog)
|
||||
exchange._api_async.load_markets = get_mock_coro(side_effect=ccxt.BaseError("deadbeef"))
|
||||
with pytest.raises(ccxt.BaseError, match="deadbeef"):
|
||||
exchange._load_async_markets()
|
||||
|
||||
|
||||
def test__load_markets(default_conf, mocker, caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
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}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
Exchange(default_conf)
|
||||
assert log_has("Unable to initialize markets.", caplog)
|
||||
assert log_has("Could not load markets.", caplog)
|
||||
|
||||
expected_return = {"ETH/BTC": "available"}
|
||||
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))
|
||||
default_conf["exchange"]["pair_whitelist"] = ["ETH/BTC"]
|
||||
ex = Exchange(default_conf)
|
||||
|
@ -563,12 +561,12 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
|
|||
start_dt = dt_now()
|
||||
time_machine.move_to(start_dt, tick=False)
|
||||
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
|
||||
exchange = get_patched_exchange(
|
||||
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.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
|
||||
exchange.reload_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
|
||||
time_machine.move_to(start_dt + timedelta(minutes=11), tick=False)
|
||||
exchange.reload_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)
|
||||
|
||||
# Not called again
|
||||
exchange._load_async_markets.reset_mock()
|
||||
lam_spy.reset_mock()
|
||||
|
||||
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):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
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
|
||||
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
|
||||
exchange.reload_markets()
|
||||
assert exchange._last_markets_refresh == 0
|
||||
assert log_has_re(r"Could not reload markets.*", caplog)
|
||||
assert exchange._last_markets_refresh == 2
|
||||
assert log_has_re(r"Could not load markets\..*", caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stake_currency", ["ETH", "BTC", "USDT"])
|
||||
def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
|
||||
default_conf["stake_currency"] = stake_currency
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/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}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
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):
|
||||
default_conf["stake_currency"] = "XRP"
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/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}.validate_pairs")
|
||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
with pytest.raises(
|
||||
ConfigurationError,
|
||||
match=r"XRP is not available as stake on .*Available currencies are: BTC, ETH, USDT",
|
||||
):
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
|
||||
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
|
||||
def test_validate_pairs(default_conf, mocker):
|
||||
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")
|
||||
type(api_mock).id = id_mock
|
||||
|
||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||
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_pricing")
|
||||
# test exchange.validate_pairs directly
|
||||
# No assert - but this should not fail (!)
|
||||
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):
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/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}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
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()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/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}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
||||
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()
|
||||
default_conf["stake_currency"] = ""
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/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}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
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
|
||||
|
||||
|
||||
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")
|
||||
api_mock = MagicMock()
|
||||
type(api_mock).load_markets = MagicMock(
|
||||
type(api_mock).load_markets = get_mock_coro(
|
||||
return_value={
|
||||
"ETH/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}.validate_timeframes")
|
||||
mocker.patch(f"{EXMS}._load_async_markets")
|
||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||
|
||||
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
|
||||
|
||||
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_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
@ -865,7 +862,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
|
|||
type(api_mock).timeframes = timeframes
|
||||
|
||||
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_stakecurrency")
|
||||
mocker.patch(f"{EXMS}.validate_pricing")
|
||||
|
@ -895,7 +892,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
|||
del api_mock.timeframes
|
||||
|
||||
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_stakecurrency")
|
||||
with pytest.raises(
|
||||
|
@ -917,7 +914,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
|||
del api_mock.timeframes
|
||||
|
||||
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_stakecurrency")
|
||||
with pytest.raises(
|
||||
|
@ -939,7 +936,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
|
|||
type(api_mock).timeframes = timeframes
|
||||
|
||||
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_stakecurrency")
|
||||
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)
|
||||
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_pairs")
|
||||
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})
|
||||
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_timeframes")
|
||||
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
|
||||
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
|
||||
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_timeframes")
|
||||
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):
|
||||
api_mock = MagicMock()
|
||||
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_timeframes")
|
||||
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
|
||||
|
||||
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(
|
||||
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.0002, 2.0, 0.01, 0.004, 0.00004),
|
||||
(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(
|
||||
|
|
|
@ -50,6 +50,7 @@ def freqai_conf(default_conf, tmp_path):
|
|||
freqaiconf.update(
|
||||
{
|
||||
"datadir": Path(default_conf["datadir"]),
|
||||
"runmode": "backtest",
|
||||
"strategy": "freqai_test_strat",
|
||||
"user_data_dir": tmp_path,
|
||||
"strategy-path": "freqtrade/tests/strategy/strats",
|
||||
|
|
|
@ -699,18 +699,20 @@ def test_process_trade_creation(
|
|||
|
||||
|
||||
def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> None:
|
||||
# TODO: Move this test to test_worker
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
fetch_ticker=ticker_usdt,
|
||||
reload_markets=MagicMock(side_effect=TemporaryError),
|
||||
reload_markets=MagicMock(),
|
||||
create_order=MagicMock(side_effect=TemporaryError),
|
||||
)
|
||||
sleep_mock = mocker.patch("time.sleep")
|
||||
|
||||
worker = Worker(args=None, config=default_conf_usdt)
|
||||
patch_get_signal(worker.freqtrade)
|
||||
mocker.patch(f"{EXMS}.reload_markets", MagicMock(side_effect=TemporaryError))
|
||||
|
||||
worker._process_running()
|
||||
assert sleep_mock.called is True
|
||||
|
|
|
@ -1650,11 +1650,11 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||
]
|
||||
args = get_args(args)
|
||||
start_backtesting(args)
|
||||
# 2 backtests, 4 tables
|
||||
# 2 backtests, 6 tables (entry, exit, mixed - each 2x)
|
||||
assert backtestmock.call_count == 2
|
||||
assert text_table_mock.call_count == 4
|
||||
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
|
||||
|
||||
# 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],
|
||||
"close_rate": [0.104969, 0.103541],
|
||||
"is_short": [False, False],
|
||||
"exit_reason": [ExitType.ROI, ExitType.ROI],
|
||||
"exit_reason": [ExitType.ROI.value, ExitType.ROI.value],
|
||||
}
|
||||
)
|
||||
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],
|
||||
"close_rate": [0.104969, 0.103541, 0.123541],
|
||||
"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(
|
||||
|
|
|
@ -415,10 +415,10 @@ def test_hyperopt_format_results(hyperopt):
|
|||
"is_short": [False, False, False, False],
|
||||
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||
"exit_reason": [
|
||||
ExitType.ROI,
|
||||
ExitType.STOP_LOSS,
|
||||
ExitType.ROI,
|
||||
ExitType.FORCE_EXIT,
|
||||
ExitType.ROI.value,
|
||||
ExitType.STOP_LOSS.value,
|
||||
ExitType.ROI.value,
|
||||
ExitType.FORCE_EXIT.value,
|
||||
],
|
||||
}
|
||||
),
|
||||
|
@ -507,10 +507,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||
"is_short": [False, False, False, False],
|
||||
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||
"exit_reason": [
|
||||
ExitType.ROI,
|
||||
ExitType.STOP_LOSS,
|
||||
ExitType.ROI,
|
||||
ExitType.FORCE_EXIT,
|
||||
ExitType.ROI.value,
|
||||
ExitType.STOP_LOSS.value,
|
||||
ExitType.ROI.value,
|
||||
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:
|
||||
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
||||
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()))
|
||||
(tmp_path / "hyperopt_results").mkdir(parents=True)
|
||||
# Dummy-reduce points to ensure scikit-learn is forced to generate new values
|
||||
|
|
|
@ -70,13 +70,13 @@ def test_text_table_bt_results():
|
|||
)
|
||||
|
||||
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"
|
||||
"|---------+-----------+----------------+------------------+"
|
||||
"|---------+----------+----------------+------------------+"
|
||||
"----------------+----------------+-------------------------|\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"
|
||||
"| TOTAL | 3 | 8.33 | 0.50000000 | "
|
||||
"| TOTAL | 3 | 8.33 | 0.50000000 | "
|
||||
"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],
|
||||
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||
"exit_reason": [
|
||||
ExitType.ROI,
|
||||
ExitType.STOP_LOSS,
|
||||
ExitType.ROI,
|
||||
ExitType.FORCE_EXIT,
|
||||
ExitType.ROI.value,
|
||||
ExitType.STOP_LOSS.value,
|
||||
ExitType.ROI.value,
|
||||
ExitType.FORCE_EXIT.value,
|
||||
],
|
||||
}
|
||||
),
|
||||
|
@ -183,10 +183,10 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
|
|||
"is_short": [False, False, False, False],
|
||||
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||
"exit_reason": [
|
||||
ExitType.ROI,
|
||||
ExitType.ROI,
|
||||
ExitType.STOP_LOSS,
|
||||
ExitType.FORCE_EXIT,
|
||||
ExitType.ROI.value,
|
||||
ExitType.ROI.value,
|
||||
ExitType.STOP_LOSS.value,
|
||||
ExitType.FORCE_EXIT.value,
|
||||
],
|
||||
}
|
||||
),
|
||||
|
@ -444,7 +444,7 @@ def test_text_table_exit_reason():
|
|||
"wins": [2, 0, 0],
|
||||
"draws": [0, 0, 0],
|
||||
"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")
|
||||
|
||||
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"
|
||||
"|----------------+-----------+----------------+------------------+"
|
||||
"|----------------+----------+----------------+------------------+"
|
||||
"----------------+----------------+-------------------------+-----------------------|\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"
|
||||
"| 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% |"
|
||||
)
|
||||
|
||||
|
|
|
@ -773,6 +773,7 @@ def test_VolumePairList_whitelist_gen(
|
|||
whitelist_result,
|
||||
caplog,
|
||||
) -> None:
|
||||
whitelist_conf["runmode"] = "backtest"
|
||||
whitelist_conf["pairlists"] = pairlists
|
||||
whitelist_conf["stake_currency"] = base_currency
|
||||
|
||||
|
@ -1270,6 +1271,7 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None:
|
|||
{"method": "StaticPairList"},
|
||||
{"method": "ShuffleFilter", "seed": 43},
|
||||
]
|
||||
whitelist_conf["runmode"] = "backtest"
|
||||
|
||||
exchange = get_patched_exchange(mocker, whitelist_conf)
|
||||
plm = PairListManager(exchange, whitelist_conf)
|
||||
|
|
177
tests/setup.Tests.ps1
Normal file
177
tests/setup.Tests.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
# pragma pylint: disable=missing-docstring, C0103
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
@ -458,55 +459,66 @@ def test_min_roi_reached3(default_conf, fee) -> None:
|
|||
ExitType.TRAILING_STOP_LOSS,
|
||||
None,
|
||||
),
|
||||
(0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 1, ExitType.NONE, None),
|
||||
(0.05, 1, ExitType.NONE, None, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None),
|
||||
(0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 0.998, ExitType.NONE, 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%
|
||||
(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.95,
|
||||
0.945,
|
||||
ExitType.NONE,
|
||||
None,
|
||||
False,
|
||||
True,
|
||||
-0.06,
|
||||
0.95,
|
||||
0.945,
|
||||
ExitType.TRAILING_STOP_LOSS,
|
||||
None,
|
||||
),
|
||||
(
|
||||
0.05,
|
||||
1,
|
||||
0.998,
|
||||
ExitType.NONE,
|
||||
None,
|
||||
False,
|
||||
True,
|
||||
-0.06,
|
||||
1,
|
||||
0.998,
|
||||
ExitType.TRAILING_STOP_LOSS,
|
||||
lambda **kwargs: -0.05,
|
||||
),
|
||||
(
|
||||
0.05,
|
||||
1,
|
||||
0.998,
|
||||
ExitType.NONE,
|
||||
None,
|
||||
False,
|
||||
True,
|
||||
0.09,
|
||||
1.04,
|
||||
1.036,
|
||||
ExitType.NONE,
|
||||
lambda **kwargs: -0.05,
|
||||
),
|
||||
(
|
||||
0.05,
|
||||
0.95,
|
||||
0.945,
|
||||
ExitType.NONE,
|
||||
None,
|
||||
False,
|
||||
True,
|
||||
0.09,
|
||||
0.98,
|
||||
0.981,
|
||||
ExitType.NONE,
|
||||
lambda current_profit, **kwargs: (
|
||||
-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,
|
||||
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(
|
||||
|
@ -552,6 +577,8 @@ def test_ft_stoploss_reached(
|
|||
exchange="binance",
|
||||
open_rate=1,
|
||||
liquidation_price=liq,
|
||||
price_precision=4,
|
||||
precision_mode=2,
|
||||
)
|
||||
trade.adjust_min_max_rates(trade.open_rate, trade.open_rate)
|
||||
strategy.trailing_stop = trailing
|
||||
|
@ -577,7 +604,7 @@ def test_ft_stoploss_reached(
|
|||
assert sl_flag.exit_flag is False
|
||||
else:
|
||||
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)
|
||||
|
||||
sl_flag = strategy.ft_stoploss_reached(
|
||||
|
@ -593,7 +620,7 @@ def test_ft_stoploss_reached(
|
|||
assert sl_flag.exit_flag is False
|
||||
else:
|
||||
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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||
def test_load_custom_strategy(default_conf, mocker, tmp_path) -> None:
|
||||
default_conf.update(
|
||||
{
|
||||
"strategy": "CustomStrategy",
|
||||
"strategy_path": "/tmp/strategies",
|
||||
"strategy_path": f"{tmp_path}/strategies",
|
||||
}
|
||||
)
|
||||
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()
|
||||
|
||||
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:
|
||||
|
@ -469,7 +469,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
|||
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)
|
||||
mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x)
|
||||
mocker.patch(
|
||||
|
@ -485,7 +485,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
|||
"--datadir",
|
||||
"/foo/bar",
|
||||
"--userdir",
|
||||
"/tmp/freqtrade",
|
||||
f"{tmp_path}/freqtrade",
|
||||
"--timeframe",
|
||||
"1m",
|
||||
"--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 "datadir" in config
|
||||
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 "timeframe" in config
|
||||
|
|
|
@ -24,16 +24,16 @@ def test_create_datadir(mocker, default_conf, caplog) -> None:
|
|||
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))
|
||||
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_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 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:
|
||||
|
@ -54,63 +54,57 @@ def test_create_userdata_dir_and_chown(mocker, tmp_path, caplog) -> None:
|
|||
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))
|
||||
md = mocker.patch.object(Path, "mkdir", MagicMock())
|
||||
|
||||
create_userdata_dir("/tmp/bar")
|
||||
create_userdata_dir(f"{tmp_path}/bar")
|
||||
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))
|
||||
md = mocker.patch.object(Path, "mkdir", MagicMock())
|
||||
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist.*"
|
||||
):
|
||||
create_userdata_dir("/tmp/bar", create_dir=False)
|
||||
with pytest.raises(OperationalException, match=r"Directory `.*.{1,2}bar` does not exist.*"):
|
||||
create_userdata_dir(f"{tmp_path}/bar", create_dir=False)
|
||||
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, "exists", MagicMock(return_value=False))
|
||||
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_args_list[0][0][1] == str(
|
||||
Path("/tmp/bar") / "strategies/sample_strategy.py"
|
||||
)
|
||||
assert copymock.call_args_list[0][0][1] == str(tmp_path / "bar/strategies/sample_strategy.py")
|
||||
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(
|
||||
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, "exists", MagicMock(return_value=False))
|
||||
mocker.patch("shutil.copy", MagicMock())
|
||||
with pytest.raises(
|
||||
OperationalException, match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist\."
|
||||
):
|
||||
copy_sample_files(Path("/tmp/bar"))
|
||||
with pytest.raises(OperationalException, match=r"Directory `.*.{1,2}bar` does not exist\."):
|
||||
copy_sample_files(Path(f"{tmp_path}/bar"))
|
||||
|
||||
mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False]))
|
||||
|
||||
with pytest.raises(
|
||||
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, "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)
|
||||
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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user