mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge remote-tracking branch 'upstream/develop' into feature/fetch-public-trades
This commit is contained in:
commit
05b2d8a2bf
|
@ -9,7 +9,7 @@ repos:
|
|||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.10.0"
|
||||
rev: "v1.10.1"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
|
@ -31,7 +31,7 @@ repos:
|
|||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.4.10'
|
||||
rev: 'v0.5.0'
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
|
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.32-cp310-cp310-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp311-cp311-linux_armv7l.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp311-cp311-linux_armv7l.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp311-cp311-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp311-cp311-win_amd64.whl
Normal file
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp312-cp312-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp312-cp312-win_amd64.whl
Normal file
Binary file not shown.
Binary file not shown.
BIN
build_helpers/TA_Lib-0.4.32-cp39-cp39-win_amd64.whl
Normal file
BIN
build_helpers/TA_Lib-0.4.32-cp39-cp39-win_amd64.whl
Normal file
Binary file not shown.
|
@ -650,9 +650,9 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
|
|||
* API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in dry-run mode.
|
||||
* Wallets (`/balance`) are simulated based on `dry_run_wallet`.
|
||||
* Orders are simulated, and will not be posted to the exchange.
|
||||
* Market orders fill based on orderbook volume the moment the order is placed.
|
||||
* Market orders fill based on orderbook volume the moment the order is placed, with a maximum slippage of 5%.
|
||||
* Limit orders fill once the price reaches the defined level - or time out based on `unfilledtimeout` settings.
|
||||
* Limit orders will be converted to market orders if they cross the price by more than 1%.
|
||||
* Limit orders will be converted to market orders if they cross the price by more than 1%, and will be filled immediately based regular market order rules (see point about Market orders above).
|
||||
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
|
||||
* Open orders (not trades, which are stored in the database) are kept open after bot restarts, with the assumption that they were not filled while being offline.
|
||||
|
||||
|
|
|
@ -73,11 +73,11 @@ Backtesting mode requires [downloading the necessary data](#downloading-data-to-
|
|||
|
||||
---
|
||||
|
||||
### Saving prediction data
|
||||
### Saving backtesting prediction data
|
||||
|
||||
To allow for tweaking your strategy (**not** the features!), FreqAI will automatically save the predictions during backtesting so that they can be reused for future backtests and live runs using the same `identifier` model. This provides a performance enhancement geared towards enabling **high-level hyperopting** of entry/exit criteria.
|
||||
|
||||
An additional directory called `backtesting_predictions`, which contains all the predictions stored in `hdf` format, will be created in the `unique-id` folder.
|
||||
An additional directory called `backtesting_predictions`, which contains all the predictions stored in `feather` format, will be created in the `unique-id` folder.
|
||||
|
||||
To change your **features**, you **must** set a new `identifier` in the config to signal to FreqAI to train new models.
|
||||
|
||||
|
@ -89,7 +89,6 @@ FreqAI allow you to reuse live historic predictions through the backtest paramet
|
|||
|
||||
The `--timerange` parameter must not be informed, as it will be automatically calculated through the data in the historic predictions file.
|
||||
|
||||
|
||||
### Downloading data to cover the full backtest period
|
||||
|
||||
For live/dry deployments, FreqAI will download the necessary data automatically. However, to use backtesting functionality, you need to download the necessary data using `download-data` (details [here](data-download.md#data-downloading)). You need to pay careful attention to understanding how much *additional* data needs to be downloaded to ensure that there is a sufficient amount of training data *before* the start of the backtesting time range. The amount of additional data can be roughly estimated by moving the start date of the time range backwards by `train_period_days` and the `startup_candle_count` (see the [parameter table](freqai-parameter-table.md) for detailed descriptions of these parameters) from the beginning of the desired backtesting time range.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Freqtrade bot"""
|
||||
|
||||
__version__ = "2024.6-dev"
|
||||
__version__ = "2024.7-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
This module contains the argument manager class
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from argparse import ArgumentParser, Namespace, _ArgumentGroup
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS
|
||||
from freqtrade.constants import DEFAULT_CONFIG
|
||||
|
@ -226,6 +226,19 @@ ARGS_ANALYZE_ENTRIES_EXITS = [
|
|||
"analysis_csv_path",
|
||||
]
|
||||
|
||||
|
||||
ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||
|
||||
ARGS_LOOKAHEAD_ANALYSIS = [
|
||||
a
|
||||
for a in ARGS_BACKTEST
|
||||
if a
|
||||
not in ("position_stacking", "use_max_market_positions", "backtest_cache", "backtest_breakdown")
|
||||
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
||||
|
||||
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
|
||||
|
||||
# Command level configs - keep at the bottom of the above definitions
|
||||
NO_CONF_REQURIED = [
|
||||
"convert-data",
|
||||
"convert-trade-data",
|
||||
|
@ -248,14 +261,6 @@ NO_CONF_REQURIED = [
|
|||
|
||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"]
|
||||
|
||||
ARGS_STRATEGY_UPDATER = ["strategy_list", "strategy_path", "recursive_strategy_search"]
|
||||
|
||||
ARGS_LOOKAHEAD_ANALYSIS = [
|
||||
a for a in ARGS_BACKTEST if a not in ("position_stacking", "use_max_market_positions", "cache")
|
||||
] + ["minimum_trade_amount", "targeted_trade_amount", "lookahead_analysis_exportfilename"]
|
||||
|
||||
ARGS_RECURSIVE_ANALYSIS = ["timeframe", "timerange", "dataformat_ohlcv", "pairs", "startup_candle"]
|
||||
|
||||
|
||||
class Arguments:
|
||||
"""
|
||||
|
@ -264,7 +269,7 @@ class Arguments:
|
|||
|
||||
def __init__(self, args: Optional[List[str]]) -> None:
|
||||
self.args = args
|
||||
self._parsed_arg: Optional[argparse.Namespace] = None
|
||||
self._parsed_arg: Optional[Namespace] = None
|
||||
|
||||
def get_parsed_arg(self) -> Dict[str, Any]:
|
||||
"""
|
||||
|
@ -277,7 +282,7 @@ class Arguments:
|
|||
|
||||
return vars(self._parsed_arg)
|
||||
|
||||
def _parse_args(self) -> argparse.Namespace:
|
||||
def _parse_args(self) -> Namespace:
|
||||
"""
|
||||
Parses given arguments and returns an argparse Namespace instance.
|
||||
"""
|
||||
|
@ -306,7 +311,9 @@ class Arguments:
|
|||
|
||||
return parsed_arg
|
||||
|
||||
def _build_args(self, optionlist, parser):
|
||||
def _build_args(
|
||||
self, optionlist: List[str], parser: Union[ArgumentParser, _ArgumentGroup]
|
||||
) -> None:
|
||||
for val in optionlist:
|
||||
opt = AVAILABLE_CLI_OPTIONS[val]
|
||||
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
|
||||
|
@ -317,16 +324,16 @@ class Arguments:
|
|||
:return: None
|
||||
"""
|
||||
# Build shared arguments (as group Common Options)
|
||||
_common_parser = argparse.ArgumentParser(add_help=False)
|
||||
_common_parser = ArgumentParser(add_help=False)
|
||||
group = _common_parser.add_argument_group("Common arguments")
|
||||
self._build_args(optionlist=ARGS_COMMON, parser=group)
|
||||
|
||||
_strategy_parser = argparse.ArgumentParser(add_help=False)
|
||||
_strategy_parser = ArgumentParser(add_help=False)
|
||||
strategy_group = _strategy_parser.add_argument_group("Strategy arguments")
|
||||
self._build_args(optionlist=ARGS_STRATEGY, parser=strategy_group)
|
||||
|
||||
# Build main command
|
||||
self.parser = argparse.ArgumentParser(
|
||||
self.parser = ArgumentParser(
|
||||
prog="freqtrade", description="Free, open source crypto trading bot"
|
||||
)
|
||||
self._build_args(optionlist=["version"], parser=self.parser)
|
||||
|
|
|
@ -45,7 +45,8 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
|||
"name": exchange["name"],
|
||||
**valid_entry,
|
||||
"supported": "Official" if exchange["supported"] else "",
|
||||
"trade_modes": ", ".join(
|
||||
"trade_modes": ("DEX: " if exchange["dex"] else "")
|
||||
+ ", ".join(
|
||||
(f"{a['margin_mode']} " if a["margin_mode"] else "") + a["trading_mode"]
|
||||
for a in exchange["trade_modes"]
|
||||
),
|
||||
|
|
|
@ -2,5 +2,8 @@
|
|||
Module to handle data operations for freqtrade
|
||||
"""
|
||||
|
||||
from freqtrade.data import converter
|
||||
|
||||
|
||||
# limit what's imported when using `from freqtrade.data import *`
|
||||
__all__ = ["converter"]
|
||||
|
|
|
@ -8437,7 +8437,7 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"BTC/USDT:USDT-240628": [
|
||||
"BTC/USDT:USDT-240927": [
|
||||
{
|
||||
"tier": 1.0,
|
||||
"currency": "USDT",
|
||||
|
@ -8567,7 +8567,7 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"BTC/USDT:USDT-240927": [
|
||||
"BTC/USDT:USDT-241227": [
|
||||
{
|
||||
"tier": 1.0,
|
||||
"currency": "USDT",
|
||||
|
@ -13805,7 +13805,7 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"ETH/USDT:USDT-240628": [
|
||||
"ETH/USDT:USDT-240927": [
|
||||
{
|
||||
"tier": 1.0,
|
||||
"currency": "USDT",
|
||||
|
@ -13935,7 +13935,7 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
"ETH/USDT:USDT-240927": [
|
||||
"ETH/USDT:USDT-241227": [
|
||||
{
|
||||
"tier": 1.0,
|
||||
"currency": "USDT",
|
||||
|
|
|
@ -47,7 +47,7 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool:
|
|||
f'{", ".join(available_exchanges())}'
|
||||
)
|
||||
|
||||
valid, reason = validate_exchange(exchange)
|
||||
valid, reason, _ = validate_exchange(exchange)
|
||||
if not valid:
|
||||
if check_for_bad:
|
||||
raise OperationalException(
|
||||
|
|
|
@ -53,16 +53,19 @@ def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[st
|
|||
return [x for x in exchanges if validate_exchange(x)[0]]
|
||||
|
||||
|
||||
def validate_exchange(exchange: str) -> Tuple[bool, str]:
|
||||
def validate_exchange(exchange: str) -> Tuple[bool, str, bool]:
|
||||
"""
|
||||
returns: can_use, reason
|
||||
with Reason including both missing and missing_opt
|
||||
"""
|
||||
ex_mod = getattr(ccxt, exchange.lower())()
|
||||
|
||||
if not ex_mod or not ex_mod.has:
|
||||
return False, "", False
|
||||
|
||||
result = True
|
||||
reason = ""
|
||||
if not ex_mod or not ex_mod.has:
|
||||
return False, ""
|
||||
is_dex = getattr(ex_mod, "dex", False)
|
||||
missing = [
|
||||
k
|
||||
for k, v in EXCHANGE_HAS_REQUIRED.items()
|
||||
|
@ -81,18 +84,19 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]:
|
|||
if missing_opt:
|
||||
reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. "
|
||||
|
||||
return result, reason
|
||||
return result, reason, is_dex
|
||||
|
||||
|
||||
def _build_exchange_list_entry(
|
||||
exchange_name: str, exchangeClasses: Dict[str, Any]
|
||||
) -> ValidExchangesType:
|
||||
valid, comment = validate_exchange(exchange_name)
|
||||
valid, comment, is_dex = validate_exchange(exchange_name)
|
||||
result: ValidExchangesType = {
|
||||
"name": exchange_name,
|
||||
"valid": valid,
|
||||
"supported": exchange_name.lower() in SUPPORTED_EXCHANGES,
|
||||
"comment": comment,
|
||||
"dex": is_dex,
|
||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||
}
|
||||
if resolved := exchangeClasses.get(exchange_name.lower()):
|
||||
|
|
|
@ -33,7 +33,7 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool =
|
|||
if log:
|
||||
logger.info(f'dumping json to "{filename}"')
|
||||
|
||||
with gzip.open(filename, "w") as fpz:
|
||||
with gzip.open(filename, "wt", encoding="utf-8") as fpz:
|
||||
rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE)
|
||||
else:
|
||||
if log:
|
||||
|
@ -60,7 +60,7 @@ def file_dump_joblib(filename: Path, data: Any, log: bool = True) -> None:
|
|||
logger.debug(f'done joblib dump to "{filename}"')
|
||||
|
||||
|
||||
def json_load(datafile: Union[gzip.GzipFile, TextIO]) -> Any:
|
||||
def json_load(datafile: TextIO) -> Any:
|
||||
"""
|
||||
load data with rapidjson
|
||||
Use this to have a consistent experience,
|
||||
|
@ -77,7 +77,7 @@ def file_load_json(file: Path):
|
|||
# Try gzip file first, otherwise regular json file.
|
||||
if gzipfile.is_file():
|
||||
logger.debug(f"Loading historical data from file {gzipfile}")
|
||||
with gzip.open(gzipfile) as datafile:
|
||||
with gzip.open(gzipfile, "rt", encoding="utf-8") as datafile:
|
||||
pairdata = json_load(datafile)
|
||||
elif file.is_file():
|
||||
logger.debug(f"Loading historical data from file {file}")
|
||||
|
|
|
@ -217,8 +217,6 @@ class Backtesting:
|
|||
raise OperationalException(
|
||||
"VolumePairList not allowed for backtesting. Please use StaticPairList instead."
|
||||
)
|
||||
if "PerformanceFilter" in self.pairlists.name_list:
|
||||
raise OperationalException("PerformanceFilter not allowed for backtesting.")
|
||||
|
||||
if len(self.strategylist) > 1 and "PrecisionFilter" in self.pairlists.name_list:
|
||||
raise OperationalException(
|
||||
|
|
|
@ -13,7 +13,7 @@ from freqtrade.constants import ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
from freqtrade.util import PeriodicCache, dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -21,6 +21,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class AgeFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.NO
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -7,13 +7,15 @@ from typing import List
|
|||
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, SupportsBacktesting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FullTradesFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.NO_ACTION
|
||||
|
||||
@property
|
||||
def needstickers(self) -> bool:
|
||||
"""
|
||||
|
|
|
@ -5,6 +5,7 @@ PairList Handler base class
|
|||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from copy import deepcopy
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
||||
|
||||
from freqtrade.constants import Config
|
||||
|
@ -51,8 +52,20 @@ PairlistParameter = Union[
|
|||
]
|
||||
|
||||
|
||||
class SupportsBacktesting(str, Enum):
|
||||
"""
|
||||
Enum to indicate if a Pairlist Handler supports backtesting.
|
||||
"""
|
||||
|
||||
YES = "yes"
|
||||
NO = "no"
|
||||
NO_ACTION = "no_action"
|
||||
BIASED = "biased"
|
||||
|
||||
|
||||
class IPairList(LoggingMixin, ABC):
|
||||
is_pairlist_generator = False
|
||||
supports_backtesting: SupportsBacktesting = SupportsBacktesting.NO
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
@ -11,7 +11,7 @@ from cachetools import TTLCache
|
|||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
from freqtrade.util.coin_gecko import FtCoinGeckoApi
|
||||
|
||||
|
||||
|
@ -20,6 +20,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class MarketCapPairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
supports_backtesting = SupportsBacktesting.BIASED
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
@ -7,13 +7,15 @@ from typing import Dict, List
|
|||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OffsetFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.YES
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -9,13 +9,15 @@ import pandas as pd
|
|||
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PerformanceFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.NO_ACTION
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -8,13 +8,15 @@ from typing import Optional
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import ROUND_UP
|
||||
from freqtrade.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, SupportsBacktesting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PrecisionFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.BIASED
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -7,13 +7,15 @@ from typing import Dict, Optional
|
|||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PriceFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.BIASED
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from typing import Dict, List, Optional
|
|||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -31,6 +31,7 @@ class ProducerPairList(IPairList):
|
|||
"""
|
||||
|
||||
is_pairlist_generator = True
|
||||
supports_backtesting = SupportsBacktesting.NO
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
@ -16,7 +16,7 @@ from freqtrade import __version__
|
|||
from freqtrade.configuration.load_config import CONFIG_PARSE_MODE
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
|
||||
|
||||
|
@ -25,6 +25,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class RemotePairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
# Potential winner bias
|
||||
supports_backtesting = SupportsBacktesting.BIASED
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
@ -9,7 +9,7 @@ from typing import Dict, List, Literal
|
|||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
from freqtrade.util.periodic_cache import PeriodicCache
|
||||
|
||||
|
||||
|
@ -19,6 +19,8 @@ ShuffleValues = Literal["candle", "iteration"]
|
|||
|
||||
|
||||
class ShuffleFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.YES
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -7,13 +7,15 @@ from typing import Dict, Optional
|
|||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Ticker
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SpreadFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.NO
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from copy import deepcopy
|
|||
from typing import Dict, List
|
||||
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -17,6 +17,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class StaticPairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
supports_backtesting = SupportsBacktesting.YES
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
@ -15,7 +15,7 @@ from freqtrade.constants import ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -27,6 +27,8 @@ class VolatilityFilter(IPairList):
|
|||
Filters pairs by volatility
|
||||
"""
|
||||
|
||||
supports_backtesting = SupportsBacktesting.NO
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ 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
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
from freqtrade.util import dt_now, format_ms_time
|
||||
|
||||
|
||||
|
@ -26,6 +26,7 @@ SORT_VALUES = ["quoteVolume"]
|
|||
|
||||
class VolumePairList(IPairList):
|
||||
is_pairlist_generator = True
|
||||
supports_backtesting = SupportsBacktesting.NO
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
@ -13,7 +13,7 @@ from freqtrade.constants import ListPairsWithTimeframes
|
|||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
|
||||
from freqtrade.util import dt_floor_day, dt_now, dt_ts
|
||||
|
||||
|
||||
|
@ -21,6 +21,8 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class RangeStabilityFilter(IPairList):
|
||||
supports_backtesting = SupportsBacktesting.NO
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
|
|
@ -11,10 +11,11 @@ from cachetools import TTLCache, cached
|
|||
from freqtrade.constants import Config, ListPairsWithTimeframes
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import CandleType
|
||||
from freqtrade.enums.runmode import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange.types import Tickers
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||
from freqtrade.plugins.pairlist.IPairList import IPairList, SupportsBacktesting
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.resolvers import PairListResolver
|
||||
|
||||
|
@ -57,9 +58,44 @@ class PairListManager(LoggingMixin):
|
|||
f"{invalid}."
|
||||
)
|
||||
|
||||
self._check_backtest()
|
||||
|
||||
refresh_period = config.get("pairlist_refresh_period", 3600)
|
||||
LoggingMixin.__init__(self, logger, refresh_period)
|
||||
|
||||
def _check_backtest(self) -> None:
|
||||
if self._config["runmode"] not in (RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT):
|
||||
return
|
||||
|
||||
pairlist_errors: List[str] = []
|
||||
noaction_pairlists: List[str] = []
|
||||
biased_pairlists: List[str] = []
|
||||
for pairlist_handler in self._pairlist_handlers:
|
||||
if pairlist_handler.supports_backtesting == SupportsBacktesting.NO:
|
||||
pairlist_errors.append(pairlist_handler.name)
|
||||
if pairlist_handler.supports_backtesting == SupportsBacktesting.NO_ACTION:
|
||||
noaction_pairlists.append(pairlist_handler.name)
|
||||
if pairlist_handler.supports_backtesting == SupportsBacktesting.BIASED:
|
||||
biased_pairlists.append(pairlist_handler.name)
|
||||
|
||||
if noaction_pairlists:
|
||||
logger.warning(
|
||||
f"Pairlist Handlers {', '.join(noaction_pairlists)} do not generate "
|
||||
"any changes during backtesting. While it's safe to leave them enabled, they will "
|
||||
"not behave like in dry/live modes. "
|
||||
)
|
||||
|
||||
if biased_pairlists:
|
||||
logger.warning(
|
||||
f"Pairlist Handlers {', '.join(biased_pairlists)} will introduce a lookahead bias "
|
||||
"to your backtest results, as they use today's data - which inheritly suffers from "
|
||||
"'winner bias'."
|
||||
)
|
||||
if pairlist_errors:
|
||||
raise OperationalException(
|
||||
f"Pairlist Handlers {', '.join(pairlist_errors)} do not support backtesting."
|
||||
)
|
||||
|
||||
@property
|
||||
def whitelist(self) -> List[str]:
|
||||
"""The current whitelist"""
|
||||
|
|
|
@ -1787,7 +1787,7 @@ class Telegram(RPCHandler):
|
|||
"_Bot Control_\n"
|
||||
"------------\n"
|
||||
"*/start:* `Starts the trader`\n"
|
||||
"*/stop:* Stops the trader\n"
|
||||
"*/stop:* `Stops the trader`\n"
|
||||
"*/stopentry:* `Stops entering, but handles open trades gracefully` \n"
|
||||
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
|
||||
"regardless of profit`\n"
|
||||
|
@ -1820,7 +1820,7 @@ class Telegram(RPCHandler):
|
|||
"that represents the current market direction. If no direction is provided `"
|
||||
"`the currently set market direction will be output.` \n"
|
||||
"*/list_custom_data <trade_id> <key>:* `List custom_data for Trade ID & Key combo.`\n"
|
||||
"`If no Key is supplied it will list all key-value pairs found for that Trade ID.`"
|
||||
"`If no Key is supplied it will list all key-value pairs found for that Trade ID.`\n"
|
||||
"_Statistics_\n"
|
||||
"------------\n"
|
||||
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
|
||||
|
|
|
@ -14,4 +14,5 @@ class ValidExchangesType(TypedDict):
|
|||
valid: bool
|
||||
supported: bool
|
||||
comment: str
|
||||
dex: bool
|
||||
trade_modes: List[TradeModeType]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from freqtrade_client.ft_rest_client import FtRestClient
|
||||
|
||||
|
||||
__version__ = "2024.6-dev"
|
||||
__version__ = "2024.7-dev"
|
||||
|
||||
if "dev" in __version__:
|
||||
from pathlib import Path
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Requirements for freqtrade client library
|
||||
requests==2.32.3
|
||||
python-rapidjson==1.17
|
||||
python-rapidjson==1.18
|
||||
|
|
|
@ -144,6 +144,7 @@ extend-select = [
|
|||
# "TCH", # flake8-type-checking
|
||||
"PTH", # flake8-use-pathlib
|
||||
# "RUF", # ruff
|
||||
"ASYNC", # flake8-async
|
||||
]
|
||||
|
||||
extend-ignore = [
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
-r docs/requirements-docs.txt
|
||||
|
||||
coveralls==4.0.1
|
||||
ruff==0.4.10
|
||||
mypy==1.10.0
|
||||
ruff==0.5.0
|
||||
mypy==1.10.1
|
||||
pre-commit==3.7.1
|
||||
pytest==8.2.2
|
||||
pytest-asyncio==0.23.7
|
||||
|
@ -18,7 +18,7 @@ pytest-random-order==1.1.1
|
|||
pytest-xdist==3.6.1
|
||||
isort==5.13.2
|
||||
# For datetime mocking
|
||||
time-machine==2.14.1
|
||||
time-machine==2.14.2
|
||||
|
||||
# Convert jupyter notebooks to markdown documents
|
||||
nbconvert==7.16.4
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.13.1
|
||||
scipy==1.14.0; python_version >= "3.10"
|
||||
scipy==1.13.1; python_version < "3.10"
|
||||
scikit-learn==1.5.0
|
||||
ft-scikit-optimize==0.9.2
|
||||
filelock==3.15.4
|
||||
|
|
|
@ -4,7 +4,7 @@ bottleneck==1.4.0
|
|||
numexpr==2.10.1
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==4.3.50
|
||||
ccxt==4.3.54
|
||||
cryptography==42.0.8
|
||||
aiohttp==3.9.5
|
||||
SQLAlchemy==2.0.31
|
||||
|
@ -16,7 +16,7 @@ cachetools==5.3.3
|
|||
requests==2.32.3
|
||||
urllib3==2.2.2
|
||||
jsonschema==4.22.0
|
||||
TA-Lib==0.4.31
|
||||
TA-Lib==0.4.32
|
||||
technical==1.4.3
|
||||
tabulate==0.9.0
|
||||
pycoingecko==3.1.0
|
||||
|
@ -30,7 +30,7 @@ pyarrow==16.1.0; platform_machine != 'armv7l'
|
|||
py_find_1st==1.1.6
|
||||
|
||||
# Load ticker files 30% faster
|
||||
python-rapidjson==1.17
|
||||
python-rapidjson==1.18
|
||||
# Properly format api responses
|
||||
orjson==3.10.5
|
||||
|
||||
|
@ -42,7 +42,7 @@ fastapi==0.111.0
|
|||
pydantic==2.7.4
|
||||
uvicorn==0.30.1
|
||||
pyjwt==2.8.0
|
||||
aiofiles==23.2.1
|
||||
aiofiles==24.1.0
|
||||
psutil==6.0.0
|
||||
|
||||
# Support for colorized terminal output
|
||||
|
|
|
@ -429,7 +429,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) ->
|
|||
backtesting.start()
|
||||
|
||||
|
||||
def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> None:
|
||||
def test_backtesting_no_pair_left(default_conf, mocker) -> None:
|
||||
mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True))
|
||||
mocker.patch(
|
||||
"freqtrade.data.history.history_utils.load_pair_history",
|
||||
|
@ -449,13 +449,6 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
|
|||
with pytest.raises(OperationalException, match="No pair in whitelist."):
|
||||
Backtesting(default_conf)
|
||||
|
||||
default_conf["pairlists"] = [{"method": "VolumePairList", "number_assets": 5}]
|
||||
with pytest.raises(
|
||||
OperationalException,
|
||||
match=r"VolumePairList not allowed for backtesting\..*StaticPairList.*",
|
||||
):
|
||||
Backtesting(default_conf)
|
||||
|
||||
default_conf.update(
|
||||
{
|
||||
"pairlists": [{"method": "StaticPairList"}],
|
||||
|
@ -469,7 +462,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) ->
|
|||
Backtesting(default_conf)
|
||||
|
||||
|
||||
def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None:
|
||||
def test_backtesting_pairlist_list(default_conf, mocker, tickers) -> None:
|
||||
mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True))
|
||||
mocker.patch(f"{EXMS}.get_tickers", tickers)
|
||||
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y: y)
|
||||
|
@ -495,12 +488,6 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
|
|||
):
|
||||
Backtesting(default_conf)
|
||||
|
||||
default_conf["pairlists"] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}]
|
||||
with pytest.raises(
|
||||
OperationalException, match="PerformanceFilter not allowed for backtesting."
|
||||
):
|
||||
Backtesting(default_conf)
|
||||
|
||||
default_conf["pairlists"] = [
|
||||
{"method": "StaticPairList"},
|
||||
{"method": "PrecisionFilter"},
|
||||
|
|
|
@ -38,6 +38,7 @@ TESTABLE_PAIRLISTS = [p for p in AVAILABLE_PAIRLISTS if p not in ["RemotePairLis
|
|||
|
||||
@pytest.fixture(scope="function")
|
||||
def whitelist_conf(default_conf):
|
||||
default_conf["runmode"] = "dry_run"
|
||||
default_conf["stake_currency"] = "BTC"
|
||||
default_conf["exchange"]["pair_whitelist"] = [
|
||||
"ETH/BTC",
|
||||
|
@ -68,6 +69,7 @@ def whitelist_conf(default_conf):
|
|||
|
||||
@pytest.fixture(scope="function")
|
||||
def whitelist_conf_2(default_conf):
|
||||
default_conf["runmode"] = "dry_run"
|
||||
default_conf["stake_currency"] = "BTC"
|
||||
default_conf["exchange"]["pair_whitelist"] = [
|
||||
"ETH/BTC",
|
||||
|
@ -94,6 +96,7 @@ def whitelist_conf_2(default_conf):
|
|||
|
||||
@pytest.fixture(scope="function")
|
||||
def whitelist_conf_agefilter(default_conf):
|
||||
default_conf["runmode"] = "dry_run"
|
||||
default_conf["stake_currency"] = "BTC"
|
||||
default_conf["exchange"]["pair_whitelist"] = [
|
||||
"ETH/BTC",
|
||||
|
@ -773,7 +776,7 @@ def test_VolumePairList_whitelist_gen(
|
|||
whitelist_result,
|
||||
caplog,
|
||||
) -> None:
|
||||
whitelist_conf["runmode"] = "backtest"
|
||||
whitelist_conf["runmode"] = "util_exchange"
|
||||
whitelist_conf["pairlists"] = pairlists
|
||||
whitelist_conf["stake_currency"] = base_currency
|
||||
|
||||
|
@ -2387,3 +2390,65 @@ def test_MarketCapPairList_exceptions(mocker, default_conf_usdt):
|
|||
OperationalException, match="This filter only support marketcap rank up to 250."
|
||||
):
|
||||
PairListManager(exchange, default_conf_usdt)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pairlists,expected_error,expected_warning",
|
||||
[
|
||||
(
|
||||
[{"method": "StaticPairList"}],
|
||||
None, # Error
|
||||
None, # Warning
|
||||
),
|
||||
(
|
||||
[{"method": "VolumePairList", "number_assets": 10}],
|
||||
"VolumePairList", # Error
|
||||
None, # Warning
|
||||
),
|
||||
(
|
||||
[{"method": "MarketCapPairList", "number_assets": 10}],
|
||||
None, # Error
|
||||
r"MarketCapPairList.*lookahead.*", # Warning
|
||||
),
|
||||
(
|
||||
[{"method": "StaticPairList"}, {"method": "FullTradesFilter"}],
|
||||
None, # Error
|
||||
r"FullTradesFilter do not generate.*", # Warning
|
||||
),
|
||||
( # combi, fails and warns
|
||||
[
|
||||
{"method": "VolumePairList", "number_assets": 10},
|
||||
{"method": "MarketCapPairList", "number_assets": 10},
|
||||
],
|
||||
"VolumePairList", # Error
|
||||
r"MarketCapPairList.*lookahead.*", # Warning
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_backtesting_modes(
|
||||
mocker, default_conf_usdt, pairlists, expected_error, expected_warning, caplog, markets, tickers
|
||||
):
|
||||
default_conf_usdt["runmode"] = "dry_run"
|
||||
default_conf_usdt["pairlists"] = pairlists
|
||||
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
exchange_has=MagicMock(return_value=True),
|
||||
get_tickers=tickers,
|
||||
)
|
||||
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||
|
||||
# Dry run mode - works always
|
||||
PairListManager(exchange, default_conf_usdt)
|
||||
|
||||
default_conf_usdt["runmode"] = "backtest"
|
||||
if expected_error:
|
||||
with pytest.raises(OperationalException, match=f"Pairlist Handlers {expected_error}.*"):
|
||||
PairListManager(exchange, default_conf_usdt)
|
||||
|
||||
if not expected_error:
|
||||
PairListManager(exchange, default_conf_usdt)
|
||||
|
||||
if expected_warning:
|
||||
assert log_has_re(f"Pairlist Handlers {expected_warning}", caplog)
|
||||
|
|
|
@ -2154,6 +2154,7 @@ def test_api_exchanges(botclient):
|
|||
"valid": True,
|
||||
"supported": True,
|
||||
"comment": "",
|
||||
"dex": False,
|
||||
"trade_modes": [
|
||||
{"trading_mode": "spot", "margin_mode": ""},
|
||||
{"trading_mode": "futures", "margin_mode": "isolated"},
|
||||
|
@ -2165,6 +2166,16 @@ def test_api_exchanges(botclient):
|
|||
"name": "mexc",
|
||||
"valid": True,
|
||||
"supported": False,
|
||||
"dex": False,
|
||||
"comment": "",
|
||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||
}
|
||||
waves = [x for x in response["exchanges"] if x["name"] == "wavesexchange"][0]
|
||||
assert waves == {
|
||||
"name": "wavesexchange",
|
||||
"valid": True,
|
||||
"supported": False,
|
||||
"dex": True,
|
||||
"comment": "",
|
||||
"trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user