Merge remote-tracking branch 'upstream/develop' into feature/fetch-public-trades
This commit is contained in:
@@ -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
-1
@@ -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.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
-7
@@ -253,8 +253,8 @@ 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 |
|
||||||
@@ -275,14 +275,14 @@ A backtesting result will look like that:
|
|||||||
| 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,8 +631,8 @@ 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 |
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+22
-22
@@ -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()
|
||||||
|
|||||||
+19
-2
@@ -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
|
||||||
|
|||||||
+8
-8
@@ -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,9 +70,9 @@ 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"
|
||||||
@@ -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,9 +509,9 @@ 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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user