mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge remote-tracking branch 'upstream/develop' into feature/fetch-public-trades
This commit is contained in:
commit
ffda564f05
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-20.04, ubuntu-22.04 ]
|
os: [ "ubuntu-20.04", "ubuntu-22.04", "ubuntu-24.04" ]
|
||||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -322,7 +322,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
$PSVersionTable
|
$PSVersionTable
|
||||||
Set-PSRepository psgallery -InstallationPolicy trusted
|
Set-PSRepository psgallery -InstallationPolicy trusted
|
||||||
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force
|
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force -SkipPublisherCheck
|
||||||
$Error.clear()
|
$Error.clear()
|
||||||
Invoke-Pester -Path "tests" -CI
|
Invoke-Pester -Path "tests" -CI
|
||||||
if ($Error.Length -gt 0) {exit 1}
|
if ($Error.Length -gt 0) {exit 1}
|
||||||
|
@ -533,12 +533,12 @@ jobs:
|
||||||
|
|
||||||
|
|
||||||
- name: Publish to PyPI (Test)
|
- name: Publish to PyPI (Test)
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.14
|
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||||
with:
|
with:
|
||||||
repository-url: https://test.pypi.org/legacy/
|
repository-url: https://test.pypi.org/legacy/
|
||||||
|
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.8.14
|
uses: pypa/gh-action-pypi-publish@v1.9.0
|
||||||
|
|
||||||
|
|
||||||
deploy-docker:
|
deploy-docker:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pycqa/flake8
|
- repo: https://github.com/pycqa/flake8
|
||||||
rev: "7.0.0"
|
rev: "7.1.0"
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies: [Flake8-pyproject]
|
additional_dependencies: [Flake8-pyproject]
|
||||||
|
@ -31,7 +31,7 @@ repos:
|
||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.4.7'
|
rev: 'v0.4.9'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- 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
|
# Setup env
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|
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.
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.
|
@ -253,36 +253,36 @@ A backtesting result will look like that:
|
||||||
|
|
||||||
```
|
```
|
||||||
================================================ BACKTESTING REPORT =================================================
|
================================================ BACKTESTING REPORT =================================================
|
||||||
| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|
| Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins Draws Loss Win% |
|
||||||
|:---------|--------:|---------------:|-----------------:|---------------:|:-------------|-------------------------:|
|
|----------+--------+----------------+------------------+----------------+--------------+--------------------------|
|
||||||
| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
|
| ADA/BTC | 35 | -0.11 | -0.00019428 | -1.94 | 4:35:00 | 14 0 21 40.0 |
|
||||||
| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
|
| ARK/BTC | 11 | -0.41 | -0.00022647 | -2.26 | 2:03:00 | 3 0 8 27.3 |
|
||||||
| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
|
| BTS/BTC | 32 | 0.31 | 0.00048938 | 4.89 | 5:05:00 | 18 0 14 56.2 |
|
||||||
| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
|
| DASH/BTC | 13 | -0.08 | -0.00005343 | -0.53 | 4:39:00 | 6 0 7 46.2 |
|
||||||
| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
|
| ENG/BTC | 18 | 1.36 | 0.00122807 | 12.27 | 2:50:00 | 8 0 10 44.4 |
|
||||||
| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
|
| EOS/BTC | 36 | 0.08 | 0.00015304 | 1.53 | 3:34:00 | 16 0 20 44.4 |
|
||||||
| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
|
| ETC/BTC | 26 | 0.37 | 0.00047576 | 4.75 | 6:14:00 | 11 0 15 42.3 |
|
||||||
| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
|
| ETH/BTC | 33 | 0.30 | 0.00049856 | 4.98 | 7:31:00 | 16 0 17 48.5 |
|
||||||
| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
|
| IOTA/BTC | 32 | 0.03 | 0.00005444 | 0.54 | 3:12:00 | 14 0 18 43.8 |
|
||||||
| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
|
| LSK/BTC | 15 | 1.75 | 0.00131413 | 13.13 | 2:58:00 | 6 0 9 40.0 |
|
||||||
| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
|
| LTC/BTC | 32 | -0.04 | -0.00006886 | -0.69 | 4:49:00 | 11 0 21 34.4 |
|
||||||
| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
|
| NANO/BTC | 17 | 1.26 | 0.00107058 | 10.70 | 1:55:00 | 10 0 7 58.5 |
|
||||||
| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
|
| NEO/BTC | 23 | 0.82 | 0.00094936 | 9.48 | 2:59:00 | 10 0 13 43.5 |
|
||||||
| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
|
| REQ/BTC | 9 | 1.17 | 0.00052734 | 5.27 | 3:47:00 | 4 0 5 44.4 |
|
||||||
| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
|
| XLM/BTC | 16 | 1.22 | 0.00097800 | 9.77 | 3:15:00 | 7 0 9 43.8 |
|
||||||
| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
|
| XMR/BTC | 23 | -0.18 | -0.00020696 | -2.07 | 5:30:00 | 12 0 11 52.2 |
|
||||||
| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
| XRP/BTC | 35 | 0.66 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
|
||||||
| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
| ZEC/BTC | 22 | -0.46 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
|
||||||
| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
| TOTAL | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
|
||||||
============================================= LEFT OPEN TRADES REPORT =============================================
|
============================================= LEFT OPEN TRADES REPORT =============================================
|
||||||
| Pair | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
| Pair | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Win Draw Loss Win% |
|
||||||
|:---------|---------:|---------------:|-----------------:|---------------:|:---------------|--------------------:|
|
|----------+---------+----------------+------------------+----------------+----------------+---------------------|
|
||||||
| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
| ADA/BTC | 1 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
|
||||||
| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
| LTC/BTC | 1 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
|
||||||
| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
| TOTAL | 2 | 0.78 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
|
||||||
==================== EXIT REASON STATS ====================
|
==================== EXIT REASON STATS ====================
|
||||||
| Exit Reason | Exits | Wins | Draws | Losses |
|
| Exit Reason | Exits | Wins | Draws | Losses |
|
||||||
|:-------------------|--------:|------:|-------:|--------:|
|
|--------------------+---------+-------+--------+---------|
|
||||||
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
| trailing_stop_loss | 205 | 150 | 0 | 55 |
|
||||||
| stop_loss | 166 | 0 | 0 | 166 |
|
| stop_loss | 166 | 0 | 0 | 166 |
|
||||||
| exit_signal | 56 | 36 | 0 | 20 |
|
| exit_signal | 56 | 36 | 0 | 20 |
|
||||||
|
@ -631,10 +631,10 @@ Detailed output for all strategies one after the other will be available, so mak
|
||||||
|
|
||||||
```
|
```
|
||||||
================================================== STRATEGY SUMMARY ===================================================================
|
================================================== STRATEGY SUMMARY ===================================================================
|
||||||
| Strategy | Entries | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % |
|
| Strategy | Trades | Avg Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses | Drawdown % |
|
||||||
|:------------|---------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|-------:|-----------:|
|
|-------------+---------+----------------+------------------+----------------+----------------+-------+--------+--------+------------|
|
||||||
| Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 |
|
| Strategy1 | 429 | 0.36 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 | 45.2 |
|
||||||
| Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
|
| Strategy2 | 1487 | -0.13 | -0.00988917 | -98.79 | 4:43:00 | 662 | 0 | 825 | 241.68 |
|
||||||
```
|
```
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
markdown==3.6
|
markdown==3.6
|
||||||
mkdocs==1.6.0
|
mkdocs==1.6.0
|
||||||
mkdocs-material==9.5.25
|
mkdocs-material==9.5.27
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.8.1
|
pymdown-extensions==10.8.1
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
|
|
|
@ -118,6 +118,14 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use
|
||||||
freqtrade-client --config rest_config.json <command> [optional parameters]
|
freqtrade-client --config rest_config.json <command> [optional parameters]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Commands with many arguments may require keyword arguments (for clarity) - which can be provided as follows:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade-client --config rest_config.json forceenter BTC/USDT long enter_tag=GutFeeling
|
||||||
|
```
|
||||||
|
|
||||||
|
This method will work for all arguments - check the "show" command for a list of available parameters.
|
||||||
|
|
||||||
??? Note "Programmatic use"
|
??? Note "Programmatic use"
|
||||||
The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API.
|
The `freqtrade-client` package (installable independent of freqtrade) can be used in your own scripts to interact with the freqtrade API.
|
||||||
to do so, please use the following:
|
to do so, please use the following:
|
||||||
|
|
|
@ -467,7 +467,7 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
|
||||||
|
|
||||||
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
||||||
|
|
||||||
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
|
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate + (side * candle['atr'] * 2), current_rate=current_rate, is_short=trade.is_short, leverage=trade.leverage)`.
|
||||||
For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement.
|
For futures, we need to adjust the direction (up or down), as well as adjust for leverage, since the [`custom_stoploss`](strategy-callbacks.md#custom-stoploss) callback returns the ["risk for this trade"](stoploss.md#stoploss-and-leverage) - not the relative price movement.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
|
@ -492,7 +492,8 @@ The helper function `stoploss_from_absolute()` can be used to convert from an ab
|
||||||
candle = dataframe.iloc[-1].squeeze()
|
candle = dataframe.iloc[-1].squeeze()
|
||||||
side = 1 if trade.is_short else -1
|
side = 1 if trade.is_short else -1
|
||||||
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
|
return stoploss_from_absolute(current_rate + (side * candle['atr'] * 2),
|
||||||
current_rate, is_short=trade.is_short,
|
current_rate=current_rate,
|
||||||
|
is_short=trade.is_short,
|
||||||
leverage=trade.leverage)
|
leverage=trade.leverage)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -13,28 +13,28 @@ The following attributes / properties are available for each individual trade -
|
||||||
|
|
||||||
| Attribute | DataType | Description |
|
| Attribute | DataType | Description |
|
||||||
|------------|-------------|-------------|
|
|------------|-------------|-------------|
|
||||||
`pair`| string | Pair of this trade
|
| `pair` | string | Pair of this trade. |
|
||||||
`is_open`| boolean | Is the trade currently open, or has it been concluded
|
| `is_open` | boolean | Is the trade currently open, or has it been concluded. |
|
||||||
`open_rate`| float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments)
|
| `open_rate` | float | Rate this trade was entered at (Avg. entry rate in case of trade-adjustments). |
|
||||||
`close_rate`| float | Close rate - only set when is_open = False
|
| `close_rate` | float | Close rate - only set when is_open = False. |
|
||||||
`stake_amount`| float | Amount in Stake (or Quote) currency.
|
| `stake_amount` | float | Amount in Stake (or Quote) currency. |
|
||||||
`amount`| float | Amount in Asset / Base currency that is currently owned.
|
| `amount` | float | Amount in Asset / Base currency that is currently owned. |
|
||||||
`open_date`| datetime | Timestamp when trade was opened **use `open_date_utc` instead**
|
| `open_date` | datetime | Timestamp when trade was opened **use `open_date_utc` instead** |
|
||||||
`open_date_utc`| datetime | Timestamp when trade was opened - in UTC
|
| `open_date_utc` | datetime | Timestamp when trade was opened - in UTC. |
|
||||||
`close_date`| datetime | Timestamp when trade was closed **use `close_date_utc` instead**
|
| `close_date` | datetime | Timestamp when trade was closed **use `close_date_utc` instead** |
|
||||||
`close_date_utc`| datetime | Timestamp when trade was closed - in UTC
|
| `close_date_utc` | datetime | Timestamp when trade was closed - in UTC. |
|
||||||
`close_profit`| float | Relative profit at the time of trade closure. `0.01` == 1%
|
| `close_profit` | float | Relative profit at the time of trade closure. `0.01` == 1% |
|
||||||
`close_profit_abs`| float | Absolute profit (in stake currency) at the time of trade closure.
|
| `close_profit_abs` | float | Absolute profit (in stake currency) at the time of trade closure. |
|
||||||
`leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets.
|
| `leverage` | float | Leverage used for this trade - defaults to 1.0 in spot markets. |
|
||||||
`enter_tag`| string | Tag provided on entry via the `enter_tag` column in the dataframe
|
| `enter_tag` | string | Tag provided on entry via the `enter_tag` column in the dataframe. |
|
||||||
`is_short` | boolean | True for short trades, False otherwise
|
| `is_short` | boolean | True for short trades, False otherwise. |
|
||||||
`orders` | Order[] | List of order objects attached to this trade (includes both filled and cancelled orders)
|
| `orders` | Order[] | List of order objects attached to this trade (includes both filled and cancelled orders). |
|
||||||
`date_last_filled_utc` | datetime | Time of the last filled order
|
| `date_last_filled_utc` | datetime | Time of the last filled order. |
|
||||||
`entry_side` | "buy" / "sell" | Order Side the trade was entered
|
| `entry_side` | "buy" / "sell" | Order Side the trade was entered. |
|
||||||
`exit_side` | "buy" / "sell" | Order Side that will result in a trade exit / position reduction.
|
| `exit_side` | "buy" / "sell" | Order Side that will result in a trade exit / position reduction. |
|
||||||
`trade_direction` | "long" / "short" | Trade direction in text - long or short.
|
| `trade_direction` | "long" / "short" | Trade direction in text - long or short. |
|
||||||
`nr_of_successful_entries` | int | Number of successful (filled) entry orders
|
| `nr_of_successful_entries` | int | Number of successful (filled) entry orders. |
|
||||||
`nr_of_successful_exits` | int | Number of successful (filled) exit orders
|
| `nr_of_successful_exits` | int | Number of successful (filled) exit orders. |
|
||||||
|
|
||||||
## Class methods
|
## Class methods
|
||||||
|
|
||||||
|
|
|
@ -30,5 +30,5 @@ if "dev" in __version__:
|
||||||
versionfile = Path("./freqtrade_commit")
|
versionfile = Path("./freqtrade_commit")
|
||||||
if versionfile.is_file():
|
if versionfile.is_file():
|
||||||
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
||||||
except Exception:
|
except Exception: # noqa: S110
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -187,7 +187,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||||
"Insert Api server Listen Address (0.0.0.0 for docker, "
|
"Insert Api server Listen Address (0.0.0.0 for docker, "
|
||||||
"otherwise best left untouched)"
|
"otherwise best left untouched)"
|
||||||
),
|
),
|
||||||
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0",
|
"default": "127.0.0.1" if not running_in_docker() else "0.0.0.0", # noqa: S104
|
||||||
"when": lambda x: x["api_server"],
|
"when": lambda x: x["api_server"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
This module contains the configuration class
|
This module contains the configuration class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
@ -301,7 +302,7 @@ class Configuration:
|
||||||
|
|
||||||
# Edge section:
|
# Edge section:
|
||||||
if "stoploss_range" in self.args and self.args["stoploss_range"]:
|
if "stoploss_range" in self.args and self.args["stoploss_range"]:
|
||||||
txt_range = eval(self.args["stoploss_range"])
|
txt_range = ast.literal_eval(self.args["stoploss_range"])
|
||||||
config["edge"].update({"stoploss_range_min": txt_range[0]})
|
config["edge"].update({"stoploss_range_min": txt_range[0]})
|
||||||
config["edge"].update({"stoploss_range_max": txt_range[1]})
|
config["edge"].update({"stoploss_range_max": txt_range[1]})
|
||||||
config["edge"].update({"stoploss_range_step": txt_range[2]})
|
config["edge"].update({"stoploss_range_step": txt_range[2]})
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -271,7 +271,7 @@ class Exchange:
|
||||||
def close(self):
|
def close(self):
|
||||||
logger.debug("Exchange object destroyed, closing async loop")
|
logger.debug("Exchange object destroyed, closing async loop")
|
||||||
if (
|
if (
|
||||||
self._api_async
|
getattr(self, "_api_async", None)
|
||||||
and inspect.iscoroutinefunction(self._api_async.close)
|
and inspect.iscoroutinefunction(self._api_async.close)
|
||||||
and self._api_async.session
|
and self._api_async.session
|
||||||
):
|
):
|
||||||
|
|
|
@ -148,7 +148,8 @@ class PyTorchModelTrainer(PyTorchTrainerInterface):
|
||||||
the motivation here is that `n_steps` is easier to optimize and keep stable,
|
the motivation here is that `n_steps` is easier to optimize and keep stable,
|
||||||
across different n_obs - the number of data points.
|
across different n_obs - the number of data points.
|
||||||
"""
|
"""
|
||||||
assert isinstance(self.n_steps, int), "Either `n_steps` or `n_epochs` should be set."
|
if not isinstance(self.n_steps, int):
|
||||||
|
raise ValueError("Either `n_steps` or `n_epochs` should be set.")
|
||||||
n_batches = n_obs // self.batch_size
|
n_batches = n_obs // self.batch_size
|
||||||
n_epochs = max(self.n_steps // n_batches, 1)
|
n_epochs = max(self.n_steps // n_batches, 1)
|
||||||
if n_epochs <= 10:
|
if n_epochs <= 10:
|
||||||
|
|
|
@ -217,7 +217,7 @@ class FreqtradeBot(LoggingMixin):
|
||||||
except Exception:
|
except Exception:
|
||||||
# Exceptions here will be happening if the db disappeared.
|
# Exceptions here will be happening if the db disappeared.
|
||||||
# At which point we can no longer commit anyway.
|
# At which point we can no longer commit anyway.
|
||||||
pass
|
logger.exception("Error during cleanup")
|
||||||
|
|
||||||
def startup(self) -> None:
|
def startup(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -13,7 +13,7 @@ def get_strategy_run_id(strategy) -> str:
|
||||||
:param strategy: strategy object.
|
:param strategy: strategy object.
|
||||||
:return: hex string id.
|
:return: hex string id.
|
||||||
"""
|
"""
|
||||||
digest = hashlib.sha1()
|
digest = hashlib.sha1() # noqa: S324
|
||||||
config = deepcopy(strategy.config)
|
config = deepcopy(strategy.config)
|
||||||
|
|
||||||
# Options that have no impact on results of individual backtest.
|
# Options that have no impact on results of individual backtest.
|
||||||
|
|
|
@ -489,7 +489,7 @@ class Hyperopt:
|
||||||
)
|
)
|
||||||
|
|
||||||
def _set_random_state(self, random_state: Optional[int]) -> int:
|
def _set_random_state(self, random_state: Optional[int]) -> int:
|
||||||
return random_state or random.randint(1, 2**16 - 1)
|
return random_state or random.randint(1, 2**16 - 1) # noqa: S311
|
||||||
|
|
||||||
def advise_and_trim(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
def advise_and_trim(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
|
||||||
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
preprocessed = self.backtesting.strategy.advise_all_indicators(data)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
|
@ -20,13 +20,13 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
||||||
|
|
||||||
|
|
||||||
def _get_line_header(
|
def _get_line_header(
|
||||||
first_column: str, stake_currency: str, direction: str = "Entries"
|
first_column: Union[str, List[str]], stake_currency: str, direction: str = "Trades"
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate header lines (goes in line with _generate_result_line())
|
Generate header lines (goes in line with _generate_result_line())
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
first_column,
|
*([first_column] if isinstance(first_column, str) else first_column),
|
||||||
direction,
|
direction,
|
||||||
"Avg Profit %",
|
"Avg Profit %",
|
||||||
f"Tot Profit {stake_currency}",
|
f"Tot Profit {stake_currency}",
|
||||||
|
@ -54,7 +54,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
|
||||||
:return: pretty printed table with tabulate as string
|
:return: pretty printed table with tabulate as string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
headers = _get_line_header("Pair", stake_currency)
|
headers = _get_line_header("Pair", stake_currency, "Trades")
|
||||||
floatfmt = _get_line_floatfmt(stake_currency)
|
floatfmt = _get_line_floatfmt(stake_currency)
|
||||||
output = [
|
output = [
|
||||||
[
|
[
|
||||||
|
@ -79,20 +79,30 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
|
||||||
:param stake_currency: stake-currency - used to correctly name headers
|
:param stake_currency: stake-currency - used to correctly name headers
|
||||||
:return: pretty printed table with tabulate as string
|
:return: pretty printed table with tabulate as string
|
||||||
"""
|
"""
|
||||||
|
floatfmt = _get_line_floatfmt(stake_currency)
|
||||||
fallback: str = ""
|
fallback: str = ""
|
||||||
|
is_list = False
|
||||||
if tag_type == "enter_tag":
|
if tag_type == "enter_tag":
|
||||||
headers = _get_line_header("TAG", stake_currency)
|
headers = _get_line_header("Enter Tag", stake_currency, "Entries")
|
||||||
else:
|
elif tag_type == "exit_tag":
|
||||||
headers = _get_line_header("Exit Reason", stake_currency, "Exits")
|
headers = _get_line_header("Exit Reason", stake_currency, "Exits")
|
||||||
fallback = "exit_reason"
|
fallback = "exit_reason"
|
||||||
|
else:
|
||||||
|
# Mix tag
|
||||||
|
headers = _get_line_header(["Enter Tag", "Exit Reason"], stake_currency, "Trades")
|
||||||
|
floatfmt.insert(0, "s")
|
||||||
|
is_list = True
|
||||||
|
|
||||||
floatfmt = _get_line_floatfmt(stake_currency)
|
|
||||||
output = [
|
output = [
|
||||||
[
|
[
|
||||||
(
|
*(
|
||||||
t["key"]
|
(
|
||||||
|
(t["key"] if isinstance(t["key"], list) else [t["key"], ""])
|
||||||
|
if is_list
|
||||||
|
else [t["key"]]
|
||||||
|
)
|
||||||
if t.get("key") is not None and len(str(t["key"])) > 0
|
if t.get("key") is not None and len(str(t["key"])) > 0
|
||||||
else t.get(fallback, "OTHER")
|
else [t.get(fallback, "OTHER")]
|
||||||
),
|
),
|
||||||
t["trades"],
|
t["trades"],
|
||||||
t["profit_mean_pct"],
|
t["profit_mean_pct"],
|
||||||
|
@ -144,7 +154,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str:
|
||||||
:return: pretty printed table with tabulate as string
|
:return: pretty printed table with tabulate as string
|
||||||
"""
|
"""
|
||||||
floatfmt = _get_line_floatfmt(stake_currency)
|
floatfmt = _get_line_floatfmt(stake_currency)
|
||||||
headers = _get_line_header("Strategy", stake_currency)
|
headers = _get_line_header("Strategy", stake_currency, "Trades")
|
||||||
# _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless
|
# _get_line_header() is also used for per-pair summary. Per-pair drawdown is mostly useless
|
||||||
# therefore we slip this column in only for strategy summary here.
|
# therefore we slip this column in only for strategy summary here.
|
||||||
headers.append("Drawdown")
|
headers.append("Drawdown")
|
||||||
|
@ -380,6 +390,32 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def _show_tag_subresults(results: Dict[str, Any], stake_currency: str):
|
||||||
|
"""
|
||||||
|
Print tag subresults (enter_tag, exit_reason_summary, mix_tag_stats)
|
||||||
|
"""
|
||||||
|
if (enter_tags := results.get("results_per_enter_tag")) is not None:
|
||||||
|
table = text_table_tags("enter_tag", enter_tags, stake_currency)
|
||||||
|
|
||||||
|
if isinstance(table, str) and len(table) > 0:
|
||||||
|
print(" ENTER TAG STATS ".center(len(table.splitlines()[0]), "="))
|
||||||
|
print(table)
|
||||||
|
|
||||||
|
if (exit_reasons := results.get("exit_reason_summary")) is not None:
|
||||||
|
table = text_table_tags("exit_tag", exit_reasons, stake_currency)
|
||||||
|
|
||||||
|
if isinstance(table, str) and len(table) > 0:
|
||||||
|
print(" EXIT REASON STATS ".center(len(table.splitlines()[0]), "="))
|
||||||
|
print(table)
|
||||||
|
|
||||||
|
if (mix_tag := results.get("mix_tag_stats")) is not None:
|
||||||
|
table = text_table_tags("mix_tag", mix_tag, stake_currency)
|
||||||
|
|
||||||
|
if isinstance(table, str) and len(table) > 0:
|
||||||
|
print(" MIXED TAG STATS ".center(len(table.splitlines()[0]), "="))
|
||||||
|
print(table)
|
||||||
|
|
||||||
|
|
||||||
def show_backtest_result(
|
def show_backtest_result(
|
||||||
strategy: str, results: Dict[str, Any], stake_currency: str, backtest_breakdown: List[str]
|
strategy: str, results: Dict[str, Any], stake_currency: str, backtest_breakdown: List[str]
|
||||||
):
|
):
|
||||||
|
@ -398,19 +434,7 @@ def show_backtest_result(
|
||||||
print(" LEFT OPEN TRADES REPORT ".center(len(table.splitlines()[0]), "="))
|
print(" LEFT OPEN TRADES REPORT ".center(len(table.splitlines()[0]), "="))
|
||||||
print(table)
|
print(table)
|
||||||
|
|
||||||
if (enter_tags := results.get("results_per_enter_tag")) is not None:
|
_show_tag_subresults(results, stake_currency)
|
||||||
table = text_table_tags("enter_tag", enter_tags, stake_currency)
|
|
||||||
|
|
||||||
if isinstance(table, str) and len(table) > 0:
|
|
||||||
print(" ENTER TAG STATS ".center(len(table.splitlines()[0]), "="))
|
|
||||||
print(table)
|
|
||||||
|
|
||||||
if (exit_reasons := results.get("exit_reason_summary")) is not None:
|
|
||||||
table = text_table_tags("exit_tag", exit_reasons, stake_currency)
|
|
||||||
|
|
||||||
if isinstance(table, str) and len(table) > 0:
|
|
||||||
print(" EXIT REASON STATS ".center(len(table.splitlines()[0]), "="))
|
|
||||||
print(table)
|
|
||||||
|
|
||||||
for period in backtest_breakdown:
|
for period in backtest_breakdown:
|
||||||
if period in results.get("periodic_breakdown", {}):
|
if period in results.get("periodic_breakdown", {}):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Dict, List, Tuple, Union
|
from typing import Any, Dict, List, Literal, Tuple, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pandas import DataFrame, Series, concat, to_datetime
|
from pandas import DataFrame, Series, concat, to_datetime
|
||||||
|
@ -68,7 +68,9 @@ def generate_rejected_signals(
|
||||||
return rejected_candles_only
|
return rejected_candles_only
|
||||||
|
|
||||||
|
|
||||||
def _generate_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
|
def _generate_result_line(
|
||||||
|
result: DataFrame, starting_balance: int, first_column: Union[str, List[str]]
|
||||||
|
) -> Dict:
|
||||||
"""
|
"""
|
||||||
Generate one result dict, with "first_column" as key.
|
Generate one result dict, with "first_column" as key.
|
||||||
"""
|
"""
|
||||||
|
@ -141,7 +143,10 @@ def generate_pair_metrics(
|
||||||
|
|
||||||
|
|
||||||
def generate_tag_metrics(
|
def generate_tag_metrics(
|
||||||
tag_type: str, starting_balance: int, results: DataFrame, skip_nan: bool = False
|
tag_type: Union[Literal["enter_tag", "exit_reason"], List[Literal["enter_tag", "exit_reason"]]],
|
||||||
|
starting_balance: int,
|
||||||
|
results: DataFrame,
|
||||||
|
skip_nan: bool = False,
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Generates and returns a list of metrics for the given tag trades and the results dataframe
|
Generates and returns a list of metrics for the given tag trades and the results dataframe
|
||||||
|
@ -153,13 +158,14 @@ def generate_tag_metrics(
|
||||||
|
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
|
|
||||||
if tag_type in results.columns:
|
if all(
|
||||||
for tag, count in results[tag_type].value_counts().items():
|
tag in results.columns for tag in (tag_type if isinstance(tag_type, list) else [tag_type])
|
||||||
result = results[results[tag_type] == tag]
|
):
|
||||||
if skip_nan and result["profit_abs"].isnull().all():
|
for tags, group in results.groupby(tag_type):
|
||||||
|
if skip_nan and group["profit_abs"].isnull().all():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tabular_data.append(_generate_result_line(result, starting_balance, tag))
|
tabular_data.append(_generate_result_line(group, starting_balance, tags))
|
||||||
|
|
||||||
# Sort by total profit %:
|
# Sort by total profit %:
|
||||||
tabular_data = sorted(tabular_data, key=lambda k: k["profit_total_abs"], reverse=True)
|
tabular_data = sorted(tabular_data, key=lambda k: k["profit_total_abs"], reverse=True)
|
||||||
|
@ -378,12 +384,18 @@ def generate_strategy_stats(
|
||||||
skip_nan=False,
|
skip_nan=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
enter_tag_results = generate_tag_metrics(
|
enter_tag_stats = generate_tag_metrics(
|
||||||
"enter_tag", starting_balance=start_balance, results=results, skip_nan=False
|
"enter_tag", starting_balance=start_balance, results=results, skip_nan=False
|
||||||
)
|
)
|
||||||
exit_reason_stats = generate_tag_metrics(
|
exit_reason_stats = generate_tag_metrics(
|
||||||
"exit_reason", starting_balance=start_balance, results=results, skip_nan=False
|
"exit_reason", starting_balance=start_balance, results=results, skip_nan=False
|
||||||
)
|
)
|
||||||
|
mix_tag_stats = generate_tag_metrics(
|
||||||
|
["enter_tag", "exit_reason"],
|
||||||
|
starting_balance=start_balance,
|
||||||
|
results=results,
|
||||||
|
skip_nan=False,
|
||||||
|
)
|
||||||
left_open_results = generate_pair_metrics(
|
left_open_results = generate_pair_metrics(
|
||||||
pairlist,
|
pairlist,
|
||||||
stake_currency=stake_currency,
|
stake_currency=stake_currency,
|
||||||
|
@ -425,8 +437,9 @@ def generate_strategy_stats(
|
||||||
"best_pair": best_pair,
|
"best_pair": best_pair,
|
||||||
"worst_pair": worst_pair,
|
"worst_pair": worst_pair,
|
||||||
"results_per_pair": pair_results,
|
"results_per_pair": pair_results,
|
||||||
"results_per_enter_tag": enter_tag_results,
|
"results_per_enter_tag": enter_tag_stats,
|
||||||
"exit_reason_summary": exit_reason_stats,
|
"exit_reason_summary": exit_reason_stats,
|
||||||
|
"mix_tag_stats": mix_tag_stats,
|
||||||
"left_open_trades": left_open_results,
|
"left_open_trades": left_open_results,
|
||||||
"total_trades": len(results),
|
"total_trades": len(results),
|
||||||
"trade_count_long": len(results.loc[~results["is_short"]]),
|
"trade_count_long": len(results.loc[~results["is_short"]]),
|
||||||
|
|
|
@ -5,11 +5,11 @@ Minimum age (days listed) pair list filter
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
|
@ -21,24 +21,17 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AgeFilter(IPairList):
|
class AgeFilter(IPairList):
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
# Checked symbols cache (dictionary of ticker symbol => timestamp)
|
||||||
self._symbolsChecked: Dict[str, int] = {}
|
self._symbolsChecked: Dict[str, int] = {}
|
||||||
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400)
|
||||||
|
|
||||||
self._min_days_listed = pairlistconfig.get("min_days_listed", 10)
|
self._min_days_listed = self._pairlistconfig.get("min_days_listed", 10)
|
||||||
self._max_days_listed = pairlistconfig.get("max_days_listed")
|
self._max_days_listed = self._pairlistconfig.get("max_days_listed")
|
||||||
|
|
||||||
candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
|
candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
|
||||||
if self._min_days_listed < 1:
|
if self._min_days_listed < 1:
|
||||||
raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
|
raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
|
||||||
if self._min_days_listed > candle_limit:
|
if self._min_days_listed > candle_limit:
|
||||||
|
|
|
@ -3,9 +3,8 @@ Full trade slots pair list filter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List
|
from typing import List
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
@ -15,16 +14,6 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FullTradesFilter(IPairList):
|
class FullTradesFilter(IPairList):
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,7 +3,7 @@ PairList Handler base class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod, abstractproperty
|
from abc import ABC, abstractmethod
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
||||||
|
|
||||||
|
@ -87,7 +87,8 @@ class IPairList(LoggingMixin, ABC):
|
||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
@abstractproperty
|
@property
|
||||||
|
@abstractmethod
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
Boolean property defining if tickers are necessary.
|
Boolean property defining if tickers are necessary.
|
||||||
|
|
|
@ -5,11 +5,10 @@ Provides dynamic pair list based on Market Cap
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||||
|
@ -22,15 +21,8 @@ logger = logging.getLogger(__name__)
|
||||||
class MarketCapPairList(IPairList):
|
class MarketCapPairList(IPairList):
|
||||||
is_pairlist_generator = True
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
if "number_assets" not in self._pairlistconfig:
|
if "number_assets" not in self._pairlistconfig:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -38,14 +30,14 @@ class MarketCapPairList(IPairList):
|
||||||
'for "pairlist.config.number_assets"'
|
'for "pairlist.config.number_assets"'
|
||||||
)
|
)
|
||||||
|
|
||||||
self._stake_currency = config["stake_currency"]
|
self._stake_currency = self._config["stake_currency"]
|
||||||
self._number_assets = self._pairlistconfig["number_assets"]
|
self._number_assets = self._pairlistconfig["number_assets"]
|
||||||
self._max_rank = self._pairlistconfig.get("max_rank", 30)
|
self._max_rank = self._pairlistconfig.get("max_rank", 30)
|
||||||
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
|
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
|
||||||
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
|
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
|
||||||
self._def_candletype = self._config["candle_type_def"]
|
self._def_candletype = self._config["candle_type_def"]
|
||||||
|
|
||||||
_coingecko_config = config.get("coingecko", {})
|
_coingecko_config = self._config.get("coingecko", {})
|
||||||
|
|
||||||
self._coingecko: FtCoinGeckoApi = FtCoinGeckoApi(
|
self._coingecko: FtCoinGeckoApi = FtCoinGeckoApi(
|
||||||
api_key=_coingecko_config.get("api_key", ""),
|
api_key=_coingecko_config.get("api_key", ""),
|
||||||
|
|
|
@ -3,9 +3,8 @@ Offset pair list filter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||||
|
@ -15,18 +14,11 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class OffsetFilter(IPairList):
|
class OffsetFilter(IPairList):
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
self._offset = pairlistconfig.get("offset", 0)
|
self._offset = self._pairlistconfig.get("offset", 0)
|
||||||
self._number_pairs = pairlistconfig.get("number_assets", 0)
|
self._number_pairs = self._pairlistconfig.get("number_assets", 0)
|
||||||
|
|
||||||
if self._offset < 0:
|
if self._offset < 0:
|
||||||
raise OperationalException("OffsetFilter requires offset to be >= 0")
|
raise OperationalException("OffsetFilter requires offset to be >= 0")
|
||||||
|
|
|
@ -3,11 +3,10 @@ Performance pair list filter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||||
|
@ -17,18 +16,11 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PerformanceFilter(IPairList):
|
class PerformanceFilter(IPairList):
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
self._minutes = pairlistconfig.get("minutes", 0)
|
self._minutes = self._pairlistconfig.get("minutes", 0)
|
||||||
self._min_profit = pairlistconfig.get("min_profit")
|
self._min_profit = self._pairlistconfig.get("min_profit")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
|
|
|
@ -3,9 +3,8 @@ Precision pair list filter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import ROUND_UP
|
from freqtrade.exchange import ROUND_UP
|
||||||
from freqtrade.exchange.types import Ticker
|
from freqtrade.exchange.types import Ticker
|
||||||
|
@ -16,15 +15,8 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PrecisionFilter(IPairList):
|
class PrecisionFilter(IPairList):
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
if "stoploss" not in self._config:
|
if "stoploss" not in self._config:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
|
|
@ -3,9 +3,8 @@ Price pair list filter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Ticker
|
from freqtrade.exchange.types import Ticker
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||||
|
@ -15,26 +14,19 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PriceFilter(IPairList):
|
class PriceFilter(IPairList):
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
self._low_price_ratio = pairlistconfig.get("low_price_ratio", 0)
|
self._low_price_ratio = self._pairlistconfig.get("low_price_ratio", 0)
|
||||||
if self._low_price_ratio < 0:
|
if self._low_price_ratio < 0:
|
||||||
raise OperationalException("PriceFilter requires low_price_ratio to be >= 0")
|
raise OperationalException("PriceFilter requires low_price_ratio to be >= 0")
|
||||||
self._min_price = pairlistconfig.get("min_price", 0)
|
self._min_price = self._pairlistconfig.get("min_price", 0)
|
||||||
if self._min_price < 0:
|
if self._min_price < 0:
|
||||||
raise OperationalException("PriceFilter requires min_price to be >= 0")
|
raise OperationalException("PriceFilter requires min_price to be >= 0")
|
||||||
self._max_price = pairlistconfig.get("max_price", 0)
|
self._max_price = self._pairlistconfig.get("max_price", 0)
|
||||||
if self._max_price < 0:
|
if self._max_price < 0:
|
||||||
raise OperationalException("PriceFilter requires max_price to be >= 0")
|
raise OperationalException("PriceFilter requires max_price to be >= 0")
|
||||||
self._max_value = pairlistconfig.get("max_value", 0)
|
self._max_value = self._pairlistconfig.get("max_value", 0)
|
||||||
if self._max_value < 0:
|
if self._max_value < 0:
|
||||||
raise OperationalException("PriceFilter requires max_value to be >= 0")
|
raise OperationalException("PriceFilter requires max_value to be >= 0")
|
||||||
self._enabled = (
|
self._enabled = (
|
||||||
|
|
|
@ -5,7 +5,7 @@ Provides pair list from Leader data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
|
@ -32,19 +32,12 @@ class ProducerPairList(IPairList):
|
||||||
|
|
||||||
is_pairlist_generator = True
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Dict[str, Any],
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
self._num_assets: int = self._pairlistconfig.get("number_assets", 0)
|
self._num_assets: int = self._pairlistconfig.get("number_assets", 0)
|
||||||
self._producer_name = self._pairlistconfig.get("producer_name", "default")
|
self._producer_name = self._pairlistconfig.get("producer_name", "default")
|
||||||
if not config.get("external_message_consumer", {}).get("enabled"):
|
if not self._config.get("external_message_consumer", {}).get("enabled"):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"ProducerPairList requires external_message_consumer to be enabled."
|
"ProducerPairList requires external_message_consumer to be enabled."
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,7 +14,6 @@ from cachetools import TTLCache
|
||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
from freqtrade.configuration.load_config import CONFIG_PARSE_MODE
|
from freqtrade.configuration.load_config import CONFIG_PARSE_MODE
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||||
|
@ -27,15 +26,8 @@ logger = logging.getLogger(__name__)
|
||||||
class RemotePairList(IPairList):
|
class RemotePairList(IPairList):
|
||||||
is_pairlist_generator = True
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
if "number_assets" not in self._pairlistconfig:
|
if "number_assets" not in self._pairlistconfig:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
|
|
@ -4,9 +4,8 @@ Shuffle pair list filter
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from typing import Any, Dict, List, Literal
|
from typing import Dict, List, Literal
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exchange import timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_seconds
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
|
@ -20,27 +19,20 @@ ShuffleValues = Literal["candle", "iteration"]
|
||||||
|
|
||||||
|
|
||||||
class ShuffleFilter(IPairList):
|
class ShuffleFilter(IPairList):
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
# Apply seed in backtesting mode to get comparable results,
|
# Apply seed in backtesting mode to get comparable results,
|
||||||
# but not in live modes to get a non-repeating order of pairs during live modes.
|
# but not in live modes to get a non-repeating order of pairs during live modes.
|
||||||
if config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN):
|
if self._config.get("runmode") in (RunMode.LIVE, RunMode.DRY_RUN):
|
||||||
self._seed = None
|
self._seed = None
|
||||||
logger.info("Live mode detected, not applying seed.")
|
logger.info("Live mode detected, not applying seed.")
|
||||||
else:
|
else:
|
||||||
self._seed = pairlistconfig.get("seed")
|
self._seed = self._pairlistconfig.get("seed")
|
||||||
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
|
logger.info(f"Backtesting mode detected, applying seed value: {self._seed}")
|
||||||
|
|
||||||
self._random = random.Random(self._seed)
|
self._random = random.Random(self._seed) # noqa: S311
|
||||||
self._shuffle_freq: ShuffleValues = pairlistconfig.get("shuffle_frequency", "candle")
|
self._shuffle_freq: ShuffleValues = self._pairlistconfig.get("shuffle_frequency", "candle")
|
||||||
self.__pairlist_cache = PeriodicCache(
|
self.__pairlist_cache = PeriodicCache(
|
||||||
maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"])
|
maxsize=1000, ttl=timeframe_to_seconds(self._config["timeframe"])
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,9 +3,8 @@ Spread pair list filter
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Ticker
|
from freqtrade.exchange.types import Ticker
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||||
|
@ -15,17 +14,10 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SpreadFilter(IPairList):
|
class SpreadFilter(IPairList):
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
self._max_spread_ratio = pairlistconfig.get("max_spread_ratio", 0.005)
|
self._max_spread_ratio = self._pairlistconfig.get("max_spread_ratio", 0.005)
|
||||||
self._enabled = self._max_spread_ratio != 0
|
self._enabled = self._max_spread_ratio != 0
|
||||||
|
|
||||||
if not self._exchange.get_option("tickers_have_bid_ask"):
|
if not self._exchange.get_option("tickers_have_bid_ask"):
|
||||||
|
|
|
@ -6,9 +6,8 @@ Provides pair white list as it configured in config
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from freqtrade.constants import Config
|
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||||
|
|
||||||
|
@ -19,15 +18,8 @@ logger = logging.getLogger(__name__)
|
||||||
class StaticPairList(IPairList):
|
class StaticPairList(IPairList):
|
||||||
is_pairlist_generator = True
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
self._allow_inactive = self._pairlistconfig.get("allow_inactive", False)
|
self._allow_inactive = self._pairlistconfig.get("allow_inactive", False)
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,13 @@ Volatility pairlist filter
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
|
@ -27,26 +27,19 @@ class VolatilityFilter(IPairList):
|
||||||
Filters pairs by volatility
|
Filters pairs by volatility
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
self._days = pairlistconfig.get("lookback_days", 10)
|
self._days = self._pairlistconfig.get("lookback_days", 10)
|
||||||
self._min_volatility = pairlistconfig.get("min_volatility", 0)
|
self._min_volatility = self._pairlistconfig.get("min_volatility", 0)
|
||||||
self._max_volatility = pairlistconfig.get("max_volatility", sys.maxsize)
|
self._max_volatility = self._pairlistconfig.get("max_volatility", sys.maxsize)
|
||||||
self._refresh_period = pairlistconfig.get("refresh_period", 1440)
|
self._refresh_period = self._pairlistconfig.get("refresh_period", 1440)
|
||||||
self._def_candletype = self._config["candle_type_def"]
|
self._def_candletype = self._config["candle_type_def"]
|
||||||
self._sort_direction: Optional[str] = pairlistconfig.get("sort_direction", None)
|
self._sort_direction: Optional[str] = self._pairlistconfig.get("sort_direction", None)
|
||||||
|
|
||||||
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
||||||
|
|
||||||
candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
|
candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
|
||||||
if self._days < 1:
|
if self._days < 1:
|
||||||
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
|
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
|
||||||
if self._days > candle_limit:
|
if self._days > candle_limit:
|
||||||
|
|
|
@ -10,7 +10,7 @@ from typing import Any, Dict, List, Literal
|
||||||
|
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
|
@ -27,15 +27,8 @@ SORT_VALUES = ["quoteVolume"]
|
||||||
class VolumePairList(IPairList):
|
class VolumePairList(IPairList):
|
||||||
is_pairlist_generator = True
|
is_pairlist_generator = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
if "number_assets" not in self._pairlistconfig:
|
if "number_assets" not in self._pairlistconfig:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -43,7 +36,7 @@ class VolumePairList(IPairList):
|
||||||
'for "pairlist.config.number_assets"'
|
'for "pairlist.config.number_assets"'
|
||||||
)
|
)
|
||||||
|
|
||||||
self._stake_currency = config["stake_currency"]
|
self._stake_currency = self._config["stake_currency"]
|
||||||
self._number_pairs = self._pairlistconfig["number_assets"]
|
self._number_pairs = self._pairlistconfig["number_assets"]
|
||||||
self._sort_key: Literal["quoteVolume"] = self._pairlistconfig.get("sort_key", "quoteVolume")
|
self._sort_key: Literal["quoteVolume"] = self._pairlistconfig.get("sort_key", "quoteVolume")
|
||||||
self._min_value = self._pairlistconfig.get("min_value", 0)
|
self._min_value = self._pairlistconfig.get("min_value", 0)
|
||||||
|
@ -94,7 +87,7 @@ class VolumePairList(IPairList):
|
||||||
if not self._validate_keys(self._sort_key):
|
if not self._validate_keys(self._sort_key):
|
||||||
raise OperationalException(f"key {self._sort_key} not in {SORT_VALUES}")
|
raise OperationalException(f"key {self._sort_key} not in {SORT_VALUES}")
|
||||||
|
|
||||||
candle_limit = exchange.ohlcv_candle_limit(
|
candle_limit = self._exchange.ohlcv_candle_limit(
|
||||||
self._lookback_timeframe, self._config["candle_type_def"]
|
self._lookback_timeframe, self._config["candle_type_def"]
|
||||||
)
|
)
|
||||||
if self._lookback_period < 0:
|
if self._lookback_period < 0:
|
||||||
|
|
|
@ -4,12 +4,12 @@ Rate of change pairlist filter
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from cachetools import TTLCache
|
from cachetools import TTLCache
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
from freqtrade.constants import ListPairsWithTimeframes
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange.types import Tickers
|
from freqtrade.exchange.types import Tickers
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
|
@ -21,26 +21,19 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RangeStabilityFilter(IPairList):
|
class RangeStabilityFilter(IPairList):
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self,
|
super().__init__(*args, **kwargs)
|
||||||
exchange,
|
|
||||||
pairlistmanager,
|
|
||||||
config: Config,
|
|
||||||
pairlistconfig: Dict[str, Any],
|
|
||||||
pairlist_pos: int,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
|
||||||
|
|
||||||
self._days = pairlistconfig.get("lookback_days", 10)
|
self._days = self._pairlistconfig.get("lookback_days", 10)
|
||||||
self._min_rate_of_change = pairlistconfig.get("min_rate_of_change", 0.01)
|
self._min_rate_of_change = self._pairlistconfig.get("min_rate_of_change", 0.01)
|
||||||
self._max_rate_of_change = pairlistconfig.get("max_rate_of_change")
|
self._max_rate_of_change = self._pairlistconfig.get("max_rate_of_change")
|
||||||
self._refresh_period = pairlistconfig.get("refresh_period", 86400)
|
self._refresh_period = self._pairlistconfig.get("refresh_period", 86400)
|
||||||
self._def_candletype = self._config["candle_type_def"]
|
self._def_candletype = self._config["candle_type_def"]
|
||||||
self._sort_direction: Optional[str] = pairlistconfig.get("sort_direction", None)
|
self._sort_direction: Optional[str] = self._pairlistconfig.get("sort_direction", None)
|
||||||
|
|
||||||
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
||||||
|
|
||||||
candle_limit = exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
|
candle_limit = self._exchange.ohlcv_candle_limit("1d", self._config["candle_type_def"])
|
||||||
if self._days < 1:
|
if self._days < 1:
|
||||||
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
|
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
|
||||||
if self._days > candle_limit:
|
if self._days > candle_limit:
|
||||||
|
|
|
@ -31,7 +31,7 @@ security = HTTPBasic()
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
def get_user_from_token(token, secret_key: str, token_type: str = "access") -> str:
|
def get_user_from_token(token, secret_key: str, token_type: str = "access") -> str: # noqa: S107
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Could not validate credentials",
|
detail="Could not validate credentials",
|
||||||
|
@ -86,11 +86,11 @@ async def validate_ws_token(
|
||||||
await ws.close(code=status.WS_1008_POLICY_VIOLATION)
|
await ws.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||||
|
|
||||||
|
|
||||||
def create_token(data: dict, secret_key: str, token_type: str = "access") -> str:
|
def create_token(data: dict, secret_key: str, token_type: str = "access") -> str: # noqa: S107
|
||||||
to_encode = data.copy()
|
to_encode = data.copy()
|
||||||
if token_type == "access":
|
if token_type == "access": # noqa: S105
|
||||||
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
|
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
|
||||||
elif token_type == "refresh":
|
elif token_type == "refresh": # noqa: S105
|
||||||
expire = datetime.now(timezone.utc) + timedelta(days=30)
|
expire = datetime.now(timezone.utc) + timedelta(days=30)
|
||||||
else:
|
else:
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
@ -127,9 +127,15 @@ def token_login(
|
||||||
):
|
):
|
||||||
if verify_auth(api_config, form_data.username, form_data.password):
|
if verify_auth(api_config, form_data.username, form_data.password):
|
||||||
token_data = {"identity": {"u": form_data.username}}
|
token_data = {"identity": {"u": form_data.username}}
|
||||||
access_token = create_token(token_data, api_config.get("jwt_secret_key", "super-secret"))
|
access_token = create_token(
|
||||||
|
token_data,
|
||||||
|
api_config.get("jwt_secret_key", "super-secret"),
|
||||||
|
token_type="access", # noqa: S106
|
||||||
|
)
|
||||||
refresh_token = create_token(
|
refresh_token = create_token(
|
||||||
token_data, api_config.get("jwt_secret_key", "super-secret"), token_type="refresh"
|
token_data,
|
||||||
|
api_config.get("jwt_secret_key", "super-secret"),
|
||||||
|
token_type="refresh", # noqa: S106
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
|
@ -148,6 +154,8 @@ def token_refresh(token: str = Depends(oauth2_scheme), api_config=Depends(get_ap
|
||||||
u = get_user_from_token(token, api_config.get("jwt_secret_key", "super-secret"), "refresh")
|
u = get_user_from_token(token, api_config.get("jwt_secret_key", "super-secret"), "refresh")
|
||||||
token_data = {"identity": {"u": u}}
|
token_data = {"identity": {"u": u}}
|
||||||
access_token = create_token(
|
access_token = create_token(
|
||||||
token_data, api_config.get("jwt_secret_key", "super-secret"), token_type="access"
|
token_data,
|
||||||
|
api_config.get("jwt_secret_key", "super-secret"),
|
||||||
|
token_type="access", # noqa: S106
|
||||||
)
|
)
|
||||||
return {"access_token": access_token}
|
return {"access_token": access_token}
|
||||||
|
|
|
@ -1466,6 +1466,8 @@ class RPC:
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
|
|
||||||
strategy = StrategyResolver.load_strategy(config)
|
strategy = StrategyResolver.load_strategy(config)
|
||||||
|
# Manually load hyperparameters, as we don't call the bot-start callback.
|
||||||
|
strategy.ft_load_hyper_params(False)
|
||||||
|
|
||||||
if strategy.plot_config and "subplots" not in strategy.plot_config:
|
if strategy.plot_config and "subplots" not in strategy.plot_config:
|
||||||
strategy.plot_config["subplots"] = {}
|
strategy.plot_config["subplots"] = {}
|
||||||
|
|
|
@ -31,7 +31,7 @@ if "dev" in __version__:
|
||||||
versionfile = Path("./freqtrade_commit")
|
versionfile = Path("./freqtrade_commit")
|
||||||
if versionfile.is_file():
|
if versionfile.is_file():
|
||||||
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
__version__ = f"docker-{__version__}-{versionfile.read_text()[:8]}"
|
||||||
except Exception:
|
except Exception: # noqa: S110
|
||||||
pass
|
pass
|
||||||
|
|
||||||
__all__ = ["FtRestClient"]
|
__all__ = ["FtRestClient"]
|
||||||
|
|
|
@ -81,12 +81,12 @@ def print_commands():
|
||||||
print(f"{x}\n\t{doc}\n")
|
print(f"{x}\n\t{doc}\n")
|
||||||
|
|
||||||
|
|
||||||
def main_exec(args: Dict[str, Any]):
|
def main_exec(parsed: Dict[str, Any]):
|
||||||
if args.get("show"):
|
if parsed.get("show"):
|
||||||
print_commands()
|
print_commands()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
config = load_config(args["config"])
|
config = load_config(parsed["config"])
|
||||||
url = config.get("api_server", {}).get("listen_ip_address", "127.0.0.1")
|
url = config.get("api_server", {}).get("listen_ip_address", "127.0.0.1")
|
||||||
port = config.get("api_server", {}).get("listen_port", "8080")
|
port = config.get("api_server", {}).get("listen_port", "8080")
|
||||||
username = config.get("api_server", {}).get("username")
|
username = config.get("api_server", {}).get("username")
|
||||||
|
@ -96,13 +96,24 @@ def main_exec(args: Dict[str, Any]):
|
||||||
client = FtRestClient(server_url, username, password)
|
client = FtRestClient(server_url, username, password)
|
||||||
|
|
||||||
m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")]
|
m = [x for x, y in inspect.getmembers(client) if not x.startswith("_")]
|
||||||
command = args["command"]
|
command = parsed["command"]
|
||||||
if command not in m:
|
if command not in m:
|
||||||
logger.error(f"Command {command} not defined")
|
logger.error(f"Command {command} not defined")
|
||||||
print_commands()
|
print_commands()
|
||||||
return
|
return
|
||||||
|
|
||||||
print(json.dumps(getattr(client, command)(*args["command_arguments"])))
|
# Split arguments with = into key/value pairs
|
||||||
|
kwargs = {x.split("=")[0]: x.split("=")[1] for x in parsed["command_arguments"] if "=" in x}
|
||||||
|
args = [x for x in parsed["command_arguments"] if "=" not in x]
|
||||||
|
try:
|
||||||
|
res = getattr(client, command)(*args, **kwargs)
|
||||||
|
print(json.dumps(res))
|
||||||
|
except TypeError as e:
|
||||||
|
logger.error(f"Error executing command {command}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fatal Error executing command {command}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
@ -54,7 +54,7 @@ class FtRestClient:
|
||||||
# return resp.text
|
# return resp.text
|
||||||
return resp.json()
|
return resp.json()
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
logger.warning("Connection error")
|
logger.warning(f"Connection error - could not connect to {netloc}.")
|
||||||
|
|
||||||
def _get(self, apipath, params: ParamsT = None):
|
def _get(self, apipath, params: ParamsT = None):
|
||||||
return self._call("GET", apipath, params=params)
|
return self._call("GET", apipath, params=params)
|
||||||
|
@ -312,20 +312,48 @@ class FtRestClient:
|
||||||
data = {"pair": pair, "price": price}
|
data = {"pair": pair, "price": price}
|
||||||
return self._post("forcebuy", data=data)
|
return self._post("forcebuy", data=data)
|
||||||
|
|
||||||
def forceenter(self, pair, side, price=None):
|
def forceenter(
|
||||||
|
self,
|
||||||
|
pair,
|
||||||
|
side,
|
||||||
|
price=None,
|
||||||
|
*,
|
||||||
|
order_type=None,
|
||||||
|
stake_amount=None,
|
||||||
|
leverage=None,
|
||||||
|
enter_tag=None,
|
||||||
|
):
|
||||||
"""Force entering a trade
|
"""Force entering a trade
|
||||||
|
|
||||||
:param pair: Pair to buy (ETH/BTC)
|
:param pair: Pair to buy (ETH/BTC)
|
||||||
:param side: 'long' or 'short'
|
:param side: 'long' or 'short'
|
||||||
:param price: Optional - price to buy
|
:param price: Optional - price to buy
|
||||||
|
:param order_type: Optional keyword argument - 'limit' or 'market'
|
||||||
|
:param stake_amount: Optional keyword argument - stake amount (as float)
|
||||||
|
:param leverage: Optional keyword argument - leverage (as float)
|
||||||
|
:param enter_tag: Optional keyword argument - entry tag (as string, default: 'force_enter')
|
||||||
:return: json object of the trade
|
:return: json object of the trade
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
"pair": pair,
|
"pair": pair,
|
||||||
"side": side,
|
"side": side,
|
||||||
}
|
}
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
data["price"] = price
|
data["price"] = price
|
||||||
|
|
||||||
|
if order_type:
|
||||||
|
data["ordertype"] = order_type
|
||||||
|
|
||||||
|
if stake_amount:
|
||||||
|
data["stakeamount"] = stake_amount
|
||||||
|
|
||||||
|
if leverage:
|
||||||
|
data["leverage"] = leverage
|
||||||
|
|
||||||
|
if enter_tag:
|
||||||
|
data["entry_tag"] = enter_tag
|
||||||
|
|
||||||
return self._post("forceenter", data=data)
|
return self._post("forceenter", data=data)
|
||||||
|
|
||||||
def forceexit(self, tradeid, ordertype=None, amount=None):
|
def forceexit(self, tradeid, ordertype=None, amount=None):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import re
|
import re
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import ANY, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from requests.exceptions import ConnectionError
|
from requests.exceptions import ConnectionError
|
||||||
|
@ -52,70 +52,89 @@ def test_FtRestClient_call_invalid(caplog):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"method,args",
|
"method,args,kwargs",
|
||||||
[
|
[
|
||||||
("start", []),
|
("start", [], {}),
|
||||||
("stop", []),
|
("stop", [], {}),
|
||||||
("stopbuy", []),
|
("stopbuy", [], {}),
|
||||||
("reload_config", []),
|
("reload_config", [], {}),
|
||||||
("balance", []),
|
("balance", [], {}),
|
||||||
("count", []),
|
("count", [], {}),
|
||||||
("entries", []),
|
("entries", [], {}),
|
||||||
("exits", []),
|
("exits", [], {}),
|
||||||
("mix_tags", []),
|
("mix_tags", [], {}),
|
||||||
("locks", []),
|
("locks", [], {}),
|
||||||
("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"]),
|
("lock_add", ["XRP/USDT", "2024-01-01 20:00:00Z", "*", "rand"], {}),
|
||||||
("delete_lock", [2]),
|
("delete_lock", [2], {}),
|
||||||
("daily", []),
|
("daily", [], {}),
|
||||||
("daily", [15]),
|
("daily", [15], {}),
|
||||||
("weekly", []),
|
("weekly", [], {}),
|
||||||
("weekly", [15]),
|
("weekly", [15], {}),
|
||||||
("monthly", []),
|
("monthly", [], {}),
|
||||||
("monthly", [12]),
|
("monthly", [12], {}),
|
||||||
("edge", []),
|
("edge", [], {}),
|
||||||
("profit", []),
|
("profit", [], {}),
|
||||||
("stats", []),
|
("stats", [], {}),
|
||||||
("performance", []),
|
("performance", [], {}),
|
||||||
("status", []),
|
("status", [], {}),
|
||||||
("version", []),
|
("version", [], {}),
|
||||||
("show_config", []),
|
("show_config", [], {}),
|
||||||
("ping", []),
|
("ping", [], {}),
|
||||||
("logs", []),
|
("logs", [], {}),
|
||||||
("logs", [55]),
|
("logs", [55], {}),
|
||||||
("trades", []),
|
("trades", [], {}),
|
||||||
("trades", [5]),
|
("trades", [5], {}),
|
||||||
("trades", [5, 5]), # With offset
|
("trades", [5, 5], {}), # With offset
|
||||||
("trade", [1]),
|
("trade", [1], {}),
|
||||||
("delete_trade", [1]),
|
("delete_trade", [1], {}),
|
||||||
("cancel_open_order", [1]),
|
("cancel_open_order", [1], {}),
|
||||||
("whitelist", []),
|
("whitelist", [], {}),
|
||||||
("blacklist", []),
|
("blacklist", [], {}),
|
||||||
("blacklist", ["XRP/USDT"]),
|
("blacklist", ["XRP/USDT"], {}),
|
||||||
("blacklist", ["XRP/USDT", "BTC/USDT"]),
|
("blacklist", ["XRP/USDT", "BTC/USDT"], {}),
|
||||||
("forcebuy", ["XRP/USDT"]),
|
("forcebuy", ["XRP/USDT"], {}),
|
||||||
("forcebuy", ["XRP/USDT", 1.5]),
|
("forcebuy", ["XRP/USDT", 1.5], {}),
|
||||||
("forceenter", ["XRP/USDT", "short"]),
|
("forceenter", ["XRP/USDT", "short"], {}),
|
||||||
("forceenter", ["XRP/USDT", "short", 1.5]),
|
("forceenter", ["XRP/USDT", "short", 1.5], {}),
|
||||||
("forceexit", [1]),
|
("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market"}),
|
||||||
("forceexit", [1, "limit"]),
|
("forceenter", ["XRP/USDT", "short", 1.5], {"order_type": "market", "stake_amount": 100}),
|
||||||
("forceexit", [1, "limit", 100]),
|
(
|
||||||
("strategies", []),
|
"forceenter",
|
||||||
("strategy", ["sampleStrategy"]),
|
["XRP/USDT", "short", 1.5],
|
||||||
("pairlists_available", []),
|
{"order_type": "market", "stake_amount": 100, "leverage": 10.0},
|
||||||
("plot_config", []),
|
),
|
||||||
("available_pairs", []),
|
(
|
||||||
("available_pairs", ["5m"]),
|
"forceenter",
|
||||||
("pair_candles", ["XRP/USDT", "5m"]),
|
["XRP/USDT", "short", 1.5],
|
||||||
("pair_candles", ["XRP/USDT", "5m", 500]),
|
{
|
||||||
("pair_history", ["XRP/USDT", "5m", "SampleStrategy"]),
|
"order_type": "market",
|
||||||
("sysinfo", []),
|
"stake_amount": 100,
|
||||||
("health", []),
|
"leverage": 10.0,
|
||||||
|
"enter_tag": "test_force_enter",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("forceexit", [1], {}),
|
||||||
|
("forceexit", [1, "limit"], {}),
|
||||||
|
("forceexit", [1, "limit", 100], {}),
|
||||||
|
("strategies", [], {}),
|
||||||
|
("strategy", ["sampleStrategy"], {}),
|
||||||
|
("pairlists_available", [], {}),
|
||||||
|
("plot_config", [], {}),
|
||||||
|
("available_pairs", [], {}),
|
||||||
|
("available_pairs", ["5m"], {}),
|
||||||
|
("pair_candles", ["XRP/USDT", "5m"], {}),
|
||||||
|
("pair_candles", ["XRP/USDT", "5m", 500], {}),
|
||||||
|
("pair_candles", ["XRP/USDT", "5m", 500], {"columns": ["close_time,close"]}),
|
||||||
|
("pair_history", ["XRP/USDT", "5m", "SampleStrategy"], {}),
|
||||||
|
("pair_history", ["XRP/USDT", "5m"], {"strategy": "SampleStrategy"}),
|
||||||
|
("sysinfo", [], {}),
|
||||||
|
("health", [], {}),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_FtRestClient_call_explicit_methods(method, args):
|
def test_FtRestClient_call_explicit_methods(method, args, kwargs):
|
||||||
client, mock = get_rest_client()
|
client, mock = get_rest_client()
|
||||||
exec = getattr(client, method)
|
exec = getattr(client, method)
|
||||||
exec(*args)
|
exec(*args, **kwargs)
|
||||||
assert mock.call_count == 1
|
assert mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -148,3 +167,40 @@ def test_ft_client(mocker, capsys, caplog):
|
||||||
)
|
)
|
||||||
main_exec(args)
|
main_exec(args)
|
||||||
assert log_has_re("Command whatever not defined", caplog)
|
assert log_has_re("Command whatever not defined", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"params, expected_args, expected_kwargs",
|
||||||
|
[
|
||||||
|
("forceenter BTC/USDT long", ["BTC/USDT", "long"], {}),
|
||||||
|
("forceenter BTC/USDT long limit", ["BTC/USDT", "long", "limit"], {}),
|
||||||
|
(
|
||||||
|
# Skip most parameters, only providing enter_tag
|
||||||
|
"forceenter BTC/USDT long enter_tag=deadBeef",
|
||||||
|
["BTC/USDT", "long"],
|
||||||
|
{"enter_tag": "deadBeef"},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"forceenter BTC/USDT long invalid_key=123",
|
||||||
|
[],
|
||||||
|
SystemExit,
|
||||||
|
# {"invalid_key": "deadBeef"},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_ft_client_argparsing(mocker, params, expected_args, expected_kwargs, caplog):
|
||||||
|
mocked_method = params.split(" ")[0]
|
||||||
|
mocker.patch("freqtrade_client.ft_client.load_config", return_value={}, autospec=True)
|
||||||
|
mm = mocker.patch(
|
||||||
|
f"freqtrade_client.ft_client.FtRestClient.{mocked_method}", return_value={}, autospec=True
|
||||||
|
)
|
||||||
|
args = add_arguments(params.split(" "))
|
||||||
|
if isinstance(expected_kwargs, dict):
|
||||||
|
main_exec(args)
|
||||||
|
mm.assert_called_once_with(ANY, *expected_args, **expected_kwargs)
|
||||||
|
else:
|
||||||
|
with pytest.raises(expected_kwargs):
|
||||||
|
main_exec(args)
|
||||||
|
|
||||||
|
assert log_has_re(f"Error executing command {mocked_method}: got an unexpected .*", caplog)
|
||||||
|
mm.assert_not_called()
|
||||||
|
|
|
@ -138,7 +138,7 @@ extend-select = [
|
||||||
# "EXE", # flake8-executable
|
# "EXE", # flake8-executable
|
||||||
# "C4", # flake8-comprehensions
|
# "C4", # flake8-comprehensions
|
||||||
"YTT", # flake8-2020
|
"YTT", # flake8-2020
|
||||||
# "S", # flake8-bandit
|
"S", # flake8-bandit
|
||||||
# "DTZ", # flake8-datetimez
|
# "DTZ", # flake8-datetimez
|
||||||
# "RSE", # flake8-raise
|
# "RSE", # flake8-raise
|
||||||
# "TCH", # flake8-type-checking
|
# "TCH", # flake8-type-checking
|
||||||
|
@ -151,13 +151,30 @@ extend-ignore = [
|
||||||
"E272", # Multiple spaces before keyword
|
"E272", # Multiple spaces before keyword
|
||||||
"E221", # Multiple spaces before operator
|
"E221", # Multiple spaces before operator
|
||||||
"B007", # Loop control variable not used
|
"B007", # Loop control variable not used
|
||||||
|
"S603", # `subprocess` call: check for execution of untrusted input
|
||||||
|
"S607", # Starting a process with a partial executable path
|
||||||
|
"S608", # Possible SQL injection vector through string-based query construction
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.mccabe]
|
[tool.ruff.lint.mccabe]
|
||||||
max-complexity = 12
|
max-complexity = 12
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"tests/*" = ["S"]
|
"freqtrade/freqai/**/*.py" = [
|
||||||
|
"S311" # Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||||
|
]
|
||||||
|
"tests/**/*.py" = [
|
||||||
|
"S101", # allow assert in tests
|
||||||
|
"S104", # Possible binding to all interfaces
|
||||||
|
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
|
||||||
|
"S105", # Possible hardcoded password assigned to: "secret"
|
||||||
|
"S106", # Possible hardcoded password assigned to argument: "token_type"
|
||||||
|
"S110", # `try`-`except`-`pass` detected, consider logging the exception
|
||||||
|
]
|
||||||
|
|
||||||
|
"ft_client/test_client/**/*.py" = [
|
||||||
|
"S101", # allow assert in tests
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.flake8-bugbear]
|
[tool.ruff.lint.flake8-bugbear]
|
||||||
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||||
|
|
|
@ -7,10 +7,10 @@
|
||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==4.0.1
|
coveralls==4.0.1
|
||||||
ruff==0.4.7
|
ruff==0.4.9
|
||||||
mypy==1.10.0
|
mypy==1.10.0
|
||||||
pre-commit==3.7.1
|
pre-commit==3.7.1
|
||||||
pytest==8.2.1
|
pytest==8.2.2
|
||||||
pytest-asyncio==0.23.7
|
pytest-asyncio==0.23.7
|
||||||
pytest-cov==5.0.0
|
pytest-cov==5.0.0
|
||||||
pytest-mock==3.14.0
|
pytest-mock==3.14.0
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
-r requirements-freqai.txt
|
-r requirements-freqai.txt
|
||||||
|
|
||||||
# Required for freqai-rl
|
# Required for freqai-rl
|
||||||
torch==2.2.2
|
torch==2.3.1; sys_platform != 'darwin' or platform_machine != 'x86_64'
|
||||||
|
torch==2.2.2; sys_platform == 'darwin' and platform_machine == 'x86_64'
|
||||||
gymnasium==0.29.1
|
gymnasium==0.29.1
|
||||||
stable_baselines3==2.3.2
|
stable_baselines3==2.3.2
|
||||||
sb3_contrib>=2.2.1
|
sb3_contrib>=2.2.1
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
scikit-learn==1.5.0
|
scikit-learn==1.5.0
|
||||||
joblib==1.4.2
|
joblib==1.4.2
|
||||||
catboost==1.2.5; 'arm' not in platform_machine
|
catboost==1.2.5; 'arm' not in platform_machine
|
||||||
lightgbm==4.3.0
|
lightgbm==4.4.0
|
||||||
xgboost==2.0.3
|
xgboost==2.0.3
|
||||||
tensorboard==2.16.2
|
tensorboard==2.17.0
|
||||||
datasieve==0.1.7
|
datasieve==0.1.7
|
||||||
|
|
|
@ -5,4 +5,4 @@
|
||||||
scipy==1.13.1
|
scipy==1.13.1
|
||||||
scikit-learn==1.5.0
|
scikit-learn==1.5.0
|
||||||
ft-scikit-optimize==0.9.2
|
ft-scikit-optimize==0.9.2
|
||||||
filelock==3.14.0
|
filelock==3.15.1
|
||||||
|
|
|
@ -4,19 +4,19 @@ bottleneck==1.3.8
|
||||||
numexpr==2.10.0
|
numexpr==2.10.0
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.3.38
|
ccxt==4.3.46
|
||||||
cryptography==42.0.7
|
cryptography==42.0.8
|
||||||
aiohttp==3.9.5
|
aiohttp==3.9.5
|
||||||
SQLAlchemy==2.0.30
|
SQLAlchemy==2.0.30
|
||||||
python-telegram-bot==21.2
|
python-telegram-bot==21.3
|
||||||
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
# can't be hard-pinned due to telegram-bot pinning httpx with ~
|
||||||
httpx>=0.24.1
|
httpx>=0.24.1
|
||||||
humanize==4.9.0
|
humanize==4.9.0
|
||||||
cachetools==5.3.3
|
cachetools==5.3.3
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
urllib3==2.2.1
|
urllib3==2.2.2
|
||||||
jsonschema==4.22.0
|
jsonschema==4.22.0
|
||||||
TA-Lib==0.4.30
|
TA-Lib==0.4.31
|
||||||
technical==1.4.3
|
technical==1.4.3
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.1.0
|
pycoingecko==3.1.0
|
||||||
|
@ -32,14 +32,14 @@ py_find_1st==1.1.6
|
||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.17
|
python-rapidjson==1.17
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.10.3
|
orjson==3.10.5
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.111.0
|
fastapi==0.111.0
|
||||||
pydantic==2.7.2
|
pydantic==2.7.4
|
||||||
uvicorn==0.30.1
|
uvicorn==0.30.1
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
|
@ -62,4 +62,4 @@ websockets==12.0
|
||||||
janus==1.0.0
|
janus==1.0.0
|
||||||
|
|
||||||
ast-comments==1.2.2
|
ast-comments==1.2.2
|
||||||
packaging==24.0
|
packaging==24.1
|
||||||
|
|
|
@ -67,10 +67,10 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
|
||||||
"enter_tag_long_b",
|
"enter_tag_long_b",
|
||||||
],
|
],
|
||||||
"exit_reason": [
|
"exit_reason": [
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.EXIT_SIGNAL,
|
ExitType.EXIT_SIGNAL.value,
|
||||||
ExitType.STOP_LOSS,
|
ExitType.STOP_LOSS.value,
|
||||||
ExitType.TRAILING_STOP_LOSS,
|
ExitType.TRAILING_STOP_LOSS.value,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import pytest
|
import pytest
|
||||||
from numpy import NaN
|
from numpy import nan
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
|
||||||
|
@ -5014,7 +5014,7 @@ def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value,
|
||||||
(10, 0.0001, 2.0, 1.0, 0.002, 0.002),
|
(10, 0.0001, 2.0, 1.0, 0.002, 0.002),
|
||||||
(10, 0.0002, 2.0, 0.01, 0.004, 0.00004),
|
(10, 0.0002, 2.0, 0.01, 0.004, 0.00004),
|
||||||
(10, 0.0002, 2.5, None, 0.005, None),
|
(10, 0.0002, 2.5, None, 0.005, None),
|
||||||
(10, 0.0002, NaN, None, 0.0, None),
|
(10, 0.0002, nan, None, 0.0, None),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_calculate_funding_fees(
|
def test_calculate_funding_fees(
|
||||||
|
|
|
@ -1650,11 +1650,11 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
||||||
]
|
]
|
||||||
args = get_args(args)
|
args = get_args(args)
|
||||||
start_backtesting(args)
|
start_backtesting(args)
|
||||||
# 2 backtests, 4 tables
|
# 2 backtests, 6 tables (entry, exit, mixed - each 2x)
|
||||||
assert backtestmock.call_count == 2
|
assert backtestmock.call_count == 2
|
||||||
assert text_table_mock.call_count == 4
|
assert text_table_mock.call_count == 4
|
||||||
assert strattable_mock.call_count == 1
|
assert strattable_mock.call_count == 1
|
||||||
assert tag_metrics_mock.call_count == 4
|
assert tag_metrics_mock.call_count == 6
|
||||||
assert strat_summary.call_count == 1
|
assert strat_summary.call_count == 1
|
||||||
|
|
||||||
# check the logs, that will contain the backtest result
|
# check the logs, that will contain the backtest result
|
||||||
|
@ -1709,7 +1709,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
||||||
"open_rate": [0.104445, 0.10302485],
|
"open_rate": [0.104445, 0.10302485],
|
||||||
"close_rate": [0.104969, 0.103541],
|
"close_rate": [0.104969, 0.103541],
|
||||||
"is_short": [False, False],
|
"is_short": [False, False],
|
||||||
"exit_reason": [ExitType.ROI, ExitType.ROI],
|
"exit_reason": [ExitType.ROI.value, ExitType.ROI.value],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
result2 = pd.DataFrame(
|
result2 = pd.DataFrame(
|
||||||
|
@ -1729,7 +1729,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
||||||
"open_rate": [0.104445, 0.10302485, 0.122541],
|
"open_rate": [0.104445, 0.10302485, 0.122541],
|
||||||
"close_rate": [0.104969, 0.103541, 0.123541],
|
"close_rate": [0.104969, 0.103541, 0.123541],
|
||||||
"is_short": [False, False, False],
|
"is_short": [False, False, False],
|
||||||
"exit_reason": [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS],
|
"exit_reason": [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
backtestmock = MagicMock(
|
backtestmock = MagicMock(
|
||||||
|
|
|
@ -415,10 +415,10 @@ def test_hyperopt_format_results(hyperopt):
|
||||||
"is_short": [False, False, False, False],
|
"is_short": [False, False, False, False],
|
||||||
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||||
"exit_reason": [
|
"exit_reason": [
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.STOP_LOSS,
|
ExitType.STOP_LOSS.value,
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.FORCE_EXIT,
|
ExitType.FORCE_EXIT.value,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -507,10 +507,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
||||||
"is_short": [False, False, False, False],
|
"is_short": [False, False, False, False],
|
||||||
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||||
"exit_reason": [
|
"exit_reason": [
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.STOP_LOSS,
|
ExitType.STOP_LOSS.value,
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.FORCE_EXIT,
|
ExitType.FORCE_EXIT.value,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
|
@ -70,13 +70,13 @@ def test_text_table_bt_results():
|
||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
"| Pair | Entries | Avg Profit % | Tot Profit BTC | "
|
"| Pair | Trades | Avg Profit % | Tot Profit BTC | "
|
||||||
"Tot Profit % | Avg Duration | Win Draw Loss Win% |\n"
|
"Tot Profit % | Avg Duration | Win Draw Loss Win% |\n"
|
||||||
"|---------+-----------+----------------+------------------+"
|
"|---------+----------+----------------+------------------+"
|
||||||
"----------------+----------------+-------------------------|\n"
|
"----------------+----------------+-------------------------|\n"
|
||||||
"| ETH/BTC | 3 | 8.33 | 0.50000000 | "
|
"| ETH/BTC | 3 | 8.33 | 0.50000000 | "
|
||||||
"12.50 | 0:20:00 | 2 0 1 66.7 |\n"
|
"12.50 | 0:20:00 | 2 0 1 66.7 |\n"
|
||||||
"| TOTAL | 3 | 8.33 | 0.50000000 | "
|
"| TOTAL | 3 | 8.33 | 0.50000000 | "
|
||||||
"12.50 | 0:20:00 | 2 0 1 66.7 |"
|
"12.50 | 0:20:00 | 2 0 1 66.7 |"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,10 +116,10 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
|
||||||
"is_short": [False, False, False, False],
|
"is_short": [False, False, False, False],
|
||||||
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||||
"exit_reason": [
|
"exit_reason": [
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.STOP_LOSS,
|
ExitType.STOP_LOSS.value,
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.FORCE_EXIT,
|
ExitType.FORCE_EXIT.value,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -183,10 +183,10 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmp_path):
|
||||||
"is_short": [False, False, False, False],
|
"is_short": [False, False, False, False],
|
||||||
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
"stake_amount": [0.01, 0.01, 0.01, 0.01],
|
||||||
"exit_reason": [
|
"exit_reason": [
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.ROI,
|
ExitType.ROI.value,
|
||||||
ExitType.STOP_LOSS,
|
ExitType.STOP_LOSS.value,
|
||||||
ExitType.FORCE_EXIT,
|
ExitType.FORCE_EXIT.value,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@ -444,7 +444,7 @@ def test_text_table_exit_reason():
|
||||||
"wins": [2, 0, 0],
|
"wins": [2, 0, 0],
|
||||||
"draws": [0, 0, 0],
|
"draws": [0, 0, 0],
|
||||||
"losses": [0, 0, 1],
|
"losses": [0, 0, 1],
|
||||||
"exit_reason": [ExitType.ROI, ExitType.ROI, ExitType.STOP_LOSS],
|
"exit_reason": [ExitType.ROI.value, ExitType.ROI.value, ExitType.STOP_LOSS.value],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -509,13 +509,13 @@ def test_text_table_strategy(testdatadir):
|
||||||
bt_res_data_comparison = bt_res_data.pop("strategy_comparison")
|
bt_res_data_comparison = bt_res_data.pop("strategy_comparison")
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
"| Strategy | Entries | Avg Profit % | Tot Profit BTC |"
|
"| Strategy | Trades | Avg Profit % | Tot Profit BTC |"
|
||||||
" Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n"
|
" Tot Profit % | Avg Duration | Win Draw Loss Win% | Drawdown |\n"
|
||||||
"|----------------+-----------+----------------+------------------+"
|
"|----------------+----------+----------------+------------------+"
|
||||||
"----------------+----------------+-------------------------+-----------------------|\n"
|
"----------------+----------------+-------------------------+-----------------------|\n"
|
||||||
"| StrategyTestV2 | 179 | 0.08 | 0.02608550 |"
|
"| StrategyTestV2 | 179 | 0.08 | 0.02608550 |"
|
||||||
" 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n"
|
" 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |\n"
|
||||||
"| TestStrategy | 179 | 0.08 | 0.02608550 |"
|
"| TestStrategy | 179 | 0.08 | 0.02608550 |"
|
||||||
" 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |"
|
" 260.85 | 3:40:00 | 170 0 9 95.0 | 0.00308222 BTC 8.67% |"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -401,11 +401,11 @@ def test_load_dry_run(default_conf, mocker, config_value, expected, arglist) ->
|
||||||
assert validated_conf["runmode"] == (RunMode.DRY_RUN if expected else RunMode.LIVE)
|
assert validated_conf["runmode"] == (RunMode.DRY_RUN if expected else RunMode.LIVE)
|
||||||
|
|
||||||
|
|
||||||
def test_load_custom_strategy(default_conf, mocker) -> None:
|
def test_load_custom_strategy(default_conf, mocker, tmp_path) -> None:
|
||||||
default_conf.update(
|
default_conf.update(
|
||||||
{
|
{
|
||||||
"strategy": "CustomStrategy",
|
"strategy": "CustomStrategy",
|
||||||
"strategy_path": "/tmp/strategies",
|
"strategy_path": f"{tmp_path}/strategies",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
@ -415,7 +415,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
|
||||||
validated_conf = configuration.load_config()
|
validated_conf = configuration.load_config()
|
||||||
|
|
||||||
assert validated_conf.get("strategy") == "CustomStrategy"
|
assert validated_conf.get("strategy") == "CustomStrategy"
|
||||||
assert validated_conf.get("strategy_path") == "/tmp/strategies"
|
assert validated_conf.get("strategy_path") == f"{tmp_path}/strategies"
|
||||||
|
|
||||||
|
|
||||||
def test_show_info(default_conf, mocker, caplog) -> None:
|
def test_show_info(default_conf, mocker, caplog) -> None:
|
||||||
|
@ -469,7 +469,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
|
||||||
assert "timerange" not in config
|
assert "timerange" not in config
|
||||||
|
|
||||||
|
|
||||||
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
|
def test_setup_configuration_with_arguments(mocker, default_conf, caplog, tmp_path) -> None:
|
||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x)
|
mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
|
@ -485,7 +485,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||||
"--datadir",
|
"--datadir",
|
||||||
"/foo/bar",
|
"/foo/bar",
|
||||||
"--userdir",
|
"--userdir",
|
||||||
"/tmp/freqtrade",
|
f"{tmp_path}/freqtrade",
|
||||||
"--timeframe",
|
"--timeframe",
|
||||||
"1m",
|
"1m",
|
||||||
"--enable-position-stacking",
|
"--enable-position-stacking",
|
||||||
|
@ -509,7 +509,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
|
||||||
assert "pair_whitelist" in config["exchange"]
|
assert "pair_whitelist" in config["exchange"]
|
||||||
assert "datadir" in config
|
assert "datadir" in config
|
||||||
assert log_has("Using data directory: {} ...".format("/foo/bar"), caplog)
|
assert log_has("Using data directory: {} ...".format("/foo/bar"), caplog)
|
||||||
assert log_has("Using user-data directory: {} ...".format(Path("/tmp/freqtrade")), caplog)
|
assert log_has(f"Using user-data directory: {tmp_path / 'freqtrade'} ...", caplog)
|
||||||
assert "user_data_dir" in config
|
assert "user_data_dir" in config
|
||||||
|
|
||||||
assert "timeframe" in config
|
assert "timeframe" in config
|
||||||
|
|
|
@ -24,16 +24,16 @@ def test_create_datadir(mocker, default_conf, caplog) -> None:
|
||||||
assert log_has("Created data directory: /foo/bar", caplog)
|
assert log_has("Created data directory: /foo/bar", caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_create_userdata_dir(mocker, default_conf, caplog) -> None:
|
def test_create_userdata_dir(mocker, tmp_path, caplog) -> None:
|
||||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
||||||
md = mocker.patch.object(Path, "mkdir", MagicMock())
|
md = mocker.patch.object(Path, "mkdir", MagicMock())
|
||||||
|
|
||||||
x = create_userdata_dir("/tmp/bar", create_dir=True)
|
x = create_userdata_dir(tmp_path / "bar", create_dir=True)
|
||||||
assert md.call_count == 10
|
assert md.call_count == 10
|
||||||
assert md.call_args[1]["parents"] is False
|
assert md.call_args[1]["parents"] is False
|
||||||
assert log_has(f'Created user-data directory: {Path("/tmp/bar")}', caplog)
|
assert log_has(f'Created user-data directory: {tmp_path / "bar"}', caplog)
|
||||||
assert isinstance(x, Path)
|
assert isinstance(x, Path)
|
||||||
assert str(x) == str(Path("/tmp/bar"))
|
assert str(x) == str(tmp_path / "bar")
|
||||||
|
|
||||||
|
|
||||||
def test_create_userdata_dir_and_chown(mocker, tmp_path, caplog) -> None:
|
def test_create_userdata_dir_and_chown(mocker, tmp_path, caplog) -> None:
|
||||||
|
@ -54,63 +54,57 @@ def test_create_userdata_dir_and_chown(mocker, tmp_path, caplog) -> None:
|
||||||
del os.environ["FT_APP_ENV"]
|
del os.environ["FT_APP_ENV"]
|
||||||
|
|
||||||
|
|
||||||
def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None:
|
def test_create_userdata_dir_exists(mocker, tmp_path) -> None:
|
||||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
|
||||||
md = mocker.patch.object(Path, "mkdir", MagicMock())
|
md = mocker.patch.object(Path, "mkdir", MagicMock())
|
||||||
|
|
||||||
create_userdata_dir("/tmp/bar")
|
create_userdata_dir(f"{tmp_path}/bar")
|
||||||
assert md.call_count == 0
|
assert md.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None:
|
def test_create_userdata_dir_exists_exception(mocker, tmp_path) -> None:
|
||||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
||||||
md = mocker.patch.object(Path, "mkdir", MagicMock())
|
md = mocker.patch.object(Path, "mkdir", MagicMock())
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(OperationalException, match=r"Directory `.*.{1,2}bar` does not exist.*"):
|
||||||
OperationalException, match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist.*"
|
create_userdata_dir(f"{tmp_path}/bar", create_dir=False)
|
||||||
):
|
|
||||||
create_userdata_dir("/tmp/bar", create_dir=False)
|
|
||||||
assert md.call_count == 0
|
assert md.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_copy_sample_files(mocker, default_conf, caplog) -> None:
|
def test_copy_sample_files(mocker, tmp_path) -> None:
|
||||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||||
copymock = mocker.patch("shutil.copy", MagicMock())
|
copymock = mocker.patch("shutil.copy", MagicMock())
|
||||||
|
|
||||||
copy_sample_files(Path("/tmp/bar"))
|
copy_sample_files(Path(f"{tmp_path}/bar"))
|
||||||
assert copymock.call_count == 3
|
assert copymock.call_count == 3
|
||||||
assert copymock.call_args_list[0][0][1] == str(
|
assert copymock.call_args_list[0][0][1] == str(tmp_path / "bar/strategies/sample_strategy.py")
|
||||||
Path("/tmp/bar") / "strategies/sample_strategy.py"
|
|
||||||
)
|
|
||||||
assert copymock.call_args_list[1][0][1] == str(
|
assert copymock.call_args_list[1][0][1] == str(
|
||||||
Path("/tmp/bar") / "hyperopts/sample_hyperopt_loss.py"
|
tmp_path / "bar/hyperopts/sample_hyperopt_loss.py"
|
||||||
)
|
)
|
||||||
assert copymock.call_args_list[2][0][1] == str(
|
assert copymock.call_args_list[2][0][1] == str(
|
||||||
Path("/tmp/bar") / "notebooks/strategy_analysis_example.ipynb"
|
tmp_path / "bar/notebooks/strategy_analysis_example.ipynb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_copy_sample_files_errors(mocker, default_conf, caplog) -> None:
|
def test_copy_sample_files_errors(mocker, tmp_path, caplog) -> None:
|
||||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||||
mocker.patch("shutil.copy", MagicMock())
|
mocker.patch("shutil.copy", MagicMock())
|
||||||
with pytest.raises(
|
with pytest.raises(OperationalException, match=r"Directory `.*.{1,2}bar` does not exist\."):
|
||||||
OperationalException, match=r"Directory `.{1,2}tmp.{1,2}bar` does not exist\."
|
copy_sample_files(Path(f"{tmp_path}/bar"))
|
||||||
):
|
|
||||||
copy_sample_files(Path("/tmp/bar"))
|
|
||||||
|
|
||||||
mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False]))
|
mocker.patch.object(Path, "is_dir", MagicMock(side_effect=[True, False]))
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
OperationalException,
|
OperationalException,
|
||||||
match=r"Directory `.{1,2}tmp.{1,2}bar.{1,2}strategies` does not exist\.",
|
match=r"Directory `.*.{1,2}bar.{1,2}strategies` does not exist\.",
|
||||||
):
|
):
|
||||||
copy_sample_files(Path("/tmp/bar"))
|
copy_sample_files(Path(f"{tmp_path}/bar"))
|
||||||
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
|
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
|
||||||
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
|
||||||
copy_sample_files(Path("/tmp/bar"))
|
copy_sample_files(Path(f"{tmp_path}/bar"))
|
||||||
assert log_has_re(r"File `.*` exists already, not deploying sample file\.", caplog)
|
assert log_has_re(r"File `.*` exists already, not deploying sample file\.", caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
copy_sample_files(Path("/tmp/bar"), overwrite=True)
|
copy_sample_files(Path(f"{tmp_path}/bar"), overwrite=True)
|
||||||
assert log_has_re(r"File `.*` exists already, overwriting\.", caplog)
|
assert log_has_re(r"File `.*` exists already, overwriting\.", caplog)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user