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