Compare commits

...

39 Commits

Author SHA1 Message Date
Alxy Savin
3a806ef2d9
Merge d7b86ee436 into ae41ab101a 2024-09-15 14:02:47 +02:00
Matthias
ae41ab101a docs: remove skip_pair_validation - it's no longer used.
Some checks are pending
Build Documentation / Deploy Docs through mike (push) Waiting to run
2024-09-15 11:28:57 +02:00
Matthias
f4881e7c6f tests: Adjust tests for removed validate_pairlist functionality 2024-09-15 11:28:57 +02:00
Matthias
94ef4380d4 chore: remove validate_pairs from exchange class
Invalid pairs were filtered out before this was called in most cases.
in cases where it's not - regular pairlist-filtering provides proper warnings.
2024-09-15 11:28:57 +02:00
Matthias
7ebe1b8c14 chore: remove pointless validation
pairs are validated through expand_pairlist.
If they're not in markets, they'll no longer be in the
pairlist once this function function is hit.
2024-09-15 11:02:49 +02:00
Matthias
79020bba28 chore: Remove "prohibitedIn" check
it's only been used for bitrex, which does no longer exist.
apparently this was forgotten when decomissioning bittrex.
2024-09-15 10:49:26 +02:00
Matthias
95c250ebcc chore: add explaining comment 2024-09-15 10:37:28 +02:00
Matthias
bfb14614cc chore: enhance change with comment 2024-09-15 09:48:44 +02:00
Matthias
12299d4810 feat: staticPairlist to warn for invalid pairs
Warnings about invalid pairs were "covered" by the implicit
filtering of `expand_pairlist()`
2024-09-15 09:46:47 +02:00
Matthias
c67a9d4e84 docs: update pairlist creation docs 2024-09-15 09:29:45 +02:00
Matthias
af422c7cd4
Merge pull request #10645 from dxbstyle/develop
added check for Kraken exchange
2024-09-14 10:44:23 +02:00
Matthias
51bdecea53 Improve check to cover more potential api oddities 2024-09-14 10:09:15 +02:00
Matthias
0f505c6d7b Improve check to cover more potential api oddities 2024-09-14 10:04:28 +02:00
Matthias
ae72f10448
Merge pull request #10619 from KingND/pixel/feat/freqai_labels_are_okay_in_lookahead_analysis
feat: include lookahead-analysis table caption when biased_indicator is likely from FreqAI target
2024-09-14 09:51:38 +02:00
dxbstyle
ae155c78c2 added check 2024-09-09 21:29:49 +02:00
KingND
f970454cb4 chore: ruff format 2024-09-08 13:57:48 -04:00
KingND
69678574d4 fix: support python 3.9 union type hinting 2024-09-08 12:04:58 -04:00
KingND
53cab5074b chore: refactor and cleanup tests 2024-09-08 12:04:58 -04:00
KingND
c6c65b1799 chore: flake8 2024-09-08 12:04:58 -04:00
KingND
bb9f64027a chore: improve language in docs 2024-09-08 12:04:58 -04:00
KingND
5f52fc4338 feat: update lookahead-analysis doc caveats to include info regarding the false positive on FreqAI targets 2024-09-08 12:04:58 -04:00
KingND
82e30c8519 feat: if a biased_indicator starting with & appears in a lookahead-analysis, caption the table with a note that freqai targets appearing here can be ignored 2024-09-08 12:04:58 -04:00
Aleksey Savin
d7b86ee436 ruff reformatted 2024-06-01 17:16:10 +03:00
Aleksey Savin
7a04f876c3 Merge branch 'develop' into Error-Skipping 2024-06-01 17:01:47 +03:00
Aleksey Savin
0ef7e9db13
Merge branch 'develop' into Error-Skipping 2024-06-01 00:33:53 +03:00
Aleksey Savin
ac5c22d0bc ... 2024-06-01 00:32:05 +03:00
Aleksey Savin
98ef62cb46
Delete makefile 2024-06-01 00:26:05 +03:00
Aleksey Savin
45c8161448 for local testing 2024-05-31 21:07:43 +00:00
Aleksey Savin
62c64e2467 I tweaked the ensure_data_exists function so that it always works without parameter 2024-04-28 19:59:47 +00:00
Matthias
fce4536ef2 Merge branch 'develop' into Error-Skipping 2024-04-25 21:12:20 +02:00
Aleksey Savin
0008a87232 Merge remote-tracking branch 'upstream/develop' into Error-Skipping 2024-04-25 18:38:58 +03:00
Aleksey Savin
116bcc2bce check for backtesting dataframe 2024-04-25 14:35:02 +00:00
Aleksey Savin
778add6d92 default save_backtest_models sync with docs 2024-04-25 08:18:52 +00:00
Aleksey Savin
5782124df9 flake8 2024-04-23 23:18:32 +00:00
Aleksey Savin
c5e68afb2c remove union 2024-04-23 23:11:08 +00:00
Aleksey Savin
49487afc86 fix for merge 2024-04-23 22:42:04 +00:00
Aleksey Savin
4b9f0c2fc2 remove new feature flag 2024-04-23 18:48:01 +00:00
Aleksey Savin
a72587576e check and fix formating code with ruff 2024-04-18 09:27:30 +00:00
Aleksey Savin
c537c43dd0 only warn on exceptions in backtest for freqai interface 2024-04-18 09:09:01 +00:00
14 changed files with 176 additions and 257 deletions

View File

@ -222,7 +222,6 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation) <br> **Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://docs.ccxt.com/#/README?id=overriding-exchange-properties-upon-instantiation) <br> **Datatype:** Dict
| `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean | `exchange.enable_ws` | Enable the usage of Websockets for the exchange. <br>[More information](#consuming-exchange-websockets).<br>*Defaults to `true`.* <br> **Datatype:** Boolean
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean | `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`*<br> **Datatype:** Boolean
| `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float | `exchange.unknown_fee_rate` | Fallback value to use when calculating trading fees. This can be useful for exchanges which have fees in non-tradable currencies. The value provided here will be multiplied with the "fee cost".<br>*Defaults to `None`<br> **Datatype:** float
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean | `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`*<br> **Datatype:** Boolean

View File

@ -205,7 +205,7 @@ This is called with each iteration of the bot (only if the Pairlist Handler is a
It must return the resulting pairlist (which may then be passed into the chain of Pairlist Handlers). It must return the resulting pairlist (which may then be passed into the chain of Pairlist Handlers).
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the end-result is not shorter than expected. Validations are optional, the parent class exposes a `verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the end-result is not shorter than expected.
#### filter_pairlist #### filter_pairlist
@ -219,7 +219,7 @@ The default implementation in the base class simply calls the `_validate_pair()`
If overridden, it must return the resulting pairlist (which may then be passed into the next Pairlist Handler in the chain). If overridden, it must return the resulting pairlist (which may then be passed into the next Pairlist Handler in the chain).
Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the end result is not shorter than expected. Validations are optional, the parent class exposes a `verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the end result is not shorter than expected.
In `VolumePairList`, this implements different methods of sorting, does early validation so only the expected number of pairs is returned. In `VolumePairList`, this implements different methods of sorting, does early validation so only the expected number of pairs is returned.

View File

@ -55,7 +55,6 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis
By default, only currently enabled pairs are allowed. By default, only currently enabled pairs are allowed.
To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration. To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration.
This can be useful for backtesting expired pairs (like quarterly spot-markets). This can be useful for backtesting expired pairs (like quarterly spot-markets).
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist. When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist.

View File

@ -101,3 +101,4 @@ This could lead to a false-negative (the strategy will then be reported as non-b
- `lookahead-analysis` has access to everything that backtesting has too. - `lookahead-analysis` has access to everything that backtesting has too.
Please don't provoke any configs like enabling position stacking. Please don't provoke any configs like enabling position stacking.
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` amount and neither leftover money in your wallet. If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` amount and neither leftover money in your wallet.
- In the results table, the `biased_indicators` column will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased. These are not biased and can safely be ignored.

View File

@ -610,9 +610,6 @@ def download_data_main(config: Config) -> None:
if "timeframes" not in config: if "timeframes" not in config:
config["timeframes"] = DL_DATA_TIMEFRAMES config["timeframes"] = DL_DATA_TIMEFRAMES
# Manual validations of relevant settings
if not config["exchange"].get("skip_pair_validation", False):
exchange.validate_pairs(expanded_pairs)
logger.info( logger.info(
f"About to download pairs: {expanded_pairs}, " f"About to download pairs: {expanded_pairs}, "
f"intervals: {config['timeframes']} to {config['datadir']}" f"intervals: {config['timeframes']} to {config['datadir']}"

View File

@ -104,7 +104,6 @@ from freqtrade.misc import (
file_load_json, file_load_json,
safe_value_fallback2, safe_value_fallback2,
) )
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.util import dt_from_ts, dt_now from freqtrade.util import dt_from_ts, dt_now
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts, format_ms_time
from freqtrade.util.periodic_cache import PeriodicCache from freqtrade.util.periodic_cache import PeriodicCache
@ -331,8 +330,6 @@ class Exchange:
# Check if all pairs are available # Check if all pairs are available
self.validate_stakecurrency(config["stake_currency"]) self.validate_stakecurrency(config["stake_currency"])
if not config["exchange"].get("skip_pair_validation"):
self.validate_pairs(config["exchange"]["pair_whitelist"])
self.validate_ordertypes(config.get("order_types", {})) self.validate_ordertypes(config.get("order_types", {}))
self.validate_order_time_in_force(config.get("order_time_in_force", {})) self.validate_order_time_in_force(config.get("order_time_in_force", {}))
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
@ -702,54 +699,6 @@ class Exchange:
f"Available currencies are: {', '.join(quote_currencies)}" f"Available currencies are: {', '.join(quote_currencies)}"
) )
def validate_pairs(self, pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
:param pairs: list of pairs
:raise: OperationalException if one pair is not available
:return: None
"""
if not self.markets:
logger.warning("Unable to validate pairs (assuming they are correct).")
return
extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True)
invalid_pairs = []
for pair in extended_pairs:
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
if self.markets and pair not in self.markets:
raise OperationalException(
f"Pair {pair} is not available on {self.name} {self.trading_mode}. "
f"Please remove {pair} from your whitelist."
)
# From ccxt Documentation:
# markets.info: An associative array of non-common market properties,
# including fees, rates, limits and other general market information.
# The internal info array is different for each particular market,
# its contents depend on the exchange.
# It can also be a string or similar ... so we need to verify that first.
elif isinstance(self.markets[pair].get("info"), dict) and self.markets[pair].get(
"info", {}
).get("prohibitedIn", False):
# Warn users about restricted pairs in whitelist.
# We cannot determine reliably if Users are affected.
logger.warning(
f"Pair {pair} is restricted for some users on this exchange."
f"Please check if you are impacted by this restriction "
f"on the exchange and eventually remove {pair} from your whitelist."
)
if (
self._config["stake_currency"]
and self.get_pair_quote_currency(pair) != self._config["stake_currency"]
):
invalid_pairs.append(pair)
if invalid_pairs:
raise OperationalException(
f"Stake-currency '{self._config['stake_currency']}' not compatible with "
f"pair-whitelist. Please remove the following pairs: {invalid_pairs}"
)
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str: def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
""" """
Get valid pair combination of curr_1 and curr_2 by trying both combinations. Get valid pair combination of curr_1 and curr_2 by trying both combinations.

View File

@ -78,6 +78,7 @@ class Kraken(Exchange):
# x["side"], x["amount"], # x["side"], x["amount"],
) )
for x in orders for x in orders
if x["remaining"] is not None and (x["side"] == "sell" or x["price"] is not None)
] ]
for bal in balances: for bal in balances:
if not isinstance(balances[bal], dict): if not isinstance(balances[bal], dict):

View File

@ -70,7 +70,7 @@ class IFreqaiModel(ABC):
self.retrain = False self.retrain = False
self.first = True self.first = True
self.set_full_path() self.set_full_path()
self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", True) self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", False)
if self.save_backtest_models: if self.save_backtest_models:
logger.info("Backtesting module configured to save all models.") logger.info("Backtesting module configured to save all models.")
@ -258,6 +258,23 @@ class IFreqaiModel(ABC):
if self.freqai_info.get("write_metrics_to_disk", False): if self.freqai_info.get("write_metrics_to_disk", False):
self.dd.save_metric_tracker_to_disk() self.dd.save_metric_tracker_to_disk()
def _train_model(self, dataframe_train, pair, dk, tr_backtest):
try:
self.tb_logger = get_tb_logger(
self.dd.model_type, dk.data_path, self.activate_tensorboard
)
model = self.train(dataframe_train, pair, dk)
self.tb_logger.close()
return model
except Exception as msg:
logger.warning(
f"Training {pair} raised exception {msg.__class__.__name__}. "
f"from {tr_backtest.start_fmt} to {tr_backtest.stop_fmt}."
f"Message: {msg}, skipping.",
exc_info=True,
)
return None
def start_backtesting( def start_backtesting(
self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen, strategy: IStrategy self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen, strategy: IStrategy
) -> FreqaiDataKitchen: ) -> FreqaiDataKitchen:
@ -352,37 +369,26 @@ class IFreqaiModel(ABC):
if not self.model_exists(dk): if not self.model_exists(dk):
dk.find_features(dataframe_train) dk.find_features(dataframe_train)
dk.find_labels(dataframe_train) dk.find_labels(dataframe_train)
self.model = self._train_model(dataframe_train, pair, dk, tr_backtest)
try: if self.model:
self.tb_logger = get_tb_logger( self.dd.pair_dict[pair]["trained_timestamp"] = int(tr_train.stopts)
self.dd.model_type, dk.data_path, self.activate_tensorboard if self.plot_features and self.model is not None:
) plot_feature_importance(self.model, pair, dk, self.plot_features)
self.model = self.train(dataframe_train, pair, dk) if self.save_backtest_models and self.model is not None:
self.tb_logger.close() logger.info("Saving backtest model to disk.")
except Exception as msg: self.dd.save_data(self.model, pair, dk)
logger.warning( else:
f"Training {pair} raised exception {msg.__class__.__name__}. " logger.info("Saving metadata to disk.")
f"Message: {msg}, skipping.", self.dd.save_metadata(dk)
exc_info=True,
)
self.model = None
self.dd.pair_dict[pair]["trained_timestamp"] = int(tr_train.stopts)
if self.plot_features and self.model is not None:
plot_feature_importance(self.model, pair, dk, self.plot_features)
if self.save_backtest_models and self.model is not None:
logger.info("Saving backtest model to disk.")
self.dd.save_data(self.model, pair, dk)
else:
logger.info("Saving metadata to disk.")
self.dd.save_metadata(dk)
else: else:
self.model = self.dd.load_data(pair, dk) self.model = self.dd.load_data(pair, dk)
pred_df, do_preds = self.predict(dataframe_backtest, dk) if self.model and len(dataframe_backtest):
append_df = dk.get_predictions_to_append(pred_df, do_preds, dataframe_backtest) pred_df, do_preds = self.predict(dataframe_backtest, dk)
dk.append_predictions(append_df) append_df = dk.get_predictions_to_append(pred_df, do_preds, dataframe_backtest)
dk.save_backtesting_prediction(append_df) dk.append_predictions(append_df)
dk.save_backtesting_prediction(append_df)
self.backtesting_fit_live_predictions(dk) self.backtesting_fit_live_predictions(dk)
dk.fill_predictions(dataframe) dk.fill_predictions(dataframe)
@ -829,7 +835,7 @@ class IFreqaiModel(ABC):
:param pair: current pair :param pair: current pair
:return: if the data exists or not :return: if the data exists or not
""" """
if self.config.get("freqai_backtest_live_models", False) and len_dataframe_backtest == 0: if len_dataframe_backtest == 0:
logger.info( logger.info(
f"No data found for pair {pair} from " f"No data found for pair {pair} from "
f"from {tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. " f"from {tr_backtest.start_fmt} to {tr_backtest.stop_fmt}. "

View File

@ -1,7 +1,7 @@
import logging import logging
import time import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List, Union
import pandas as pd import pandas as pd
from rich.text import Text from rich.text import Text
@ -19,7 +19,9 @@ logger = logging.getLogger(__name__)
class LookaheadAnalysisSubFunctions: class LookaheadAnalysisSubFunctions:
@staticmethod @staticmethod
def text_table_lookahead_analysis_instances( def text_table_lookahead_analysis_instances(
config: Dict[str, Any], lookahead_instances: List[LookaheadAnalysis] config: Dict[str, Any],
lookahead_instances: List[LookaheadAnalysis],
caption: Union[str, None] = None,
): ):
headers = [ headers = [
"filename", "filename",
@ -65,7 +67,9 @@ class LookaheadAnalysisSubFunctions:
] ]
) )
print_rich_table(data, headers, summary="Lookahead Analysis") print_rich_table(
data, headers, summary="Lookahead Analysis", table_kwargs={"caption": caption}
)
return data return data
@staticmethod @staticmethod
@ -239,8 +243,24 @@ class LookaheadAnalysisSubFunctions:
# report the results # report the results
if lookaheadAnalysis_instances: if lookaheadAnalysis_instances:
caption: Union[str, None] = None
if any(
[
any(
[
indicator.startswith("&")
for indicator in inst.current_analysis.false_indicators
]
)
for inst in lookaheadAnalysis_instances
]
):
caption = (
"Any indicators in 'biased_indicators' which are used within "
"set_freqai_targets() can be ignored."
)
LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances( LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances(
config, lookaheadAnalysis_instances config, lookaheadAnalysis_instances, caption=caption
) )
if config.get("lookahead_analysis_exportfilename") is not None: if config.get("lookahead_analysis_exportfilename") is not None:
LookaheadAnalysisSubFunctions.export_to_csv(config, lookaheadAnalysis_instances) LookaheadAnalysisSubFunctions.export_to_csv(config, lookaheadAnalysis_instances)

View File

@ -61,14 +61,15 @@ class StaticPairList(IPairList):
:param tickers: Tickers (from exchange.get_tickers). May be cached. :param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: List of pairs :return: List of pairs
""" """
wl = self.verify_whitelist(
self._config["exchange"]["pair_whitelist"], logger.info, keep_invalid=True
)
if self._allow_inactive: if self._allow_inactive:
return self.verify_whitelist( return wl
self._config["exchange"]["pair_whitelist"], logger.info, keep_invalid=True
)
else: else:
return self._whitelist_for_active_markets( # Avoid implicit filtering of "verify_whitelist" to keep
self.verify_whitelist(self._config["exchange"]["pair_whitelist"], logger.info) # proper warnings in the log
) return self._whitelist_for_active_markets(wl)
def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]: def filter_pairlist(self, pairlist: List[str], tickers: Tickers) -> List[str]:
""" """

View File

@ -28,6 +28,7 @@ def expand_pairlist(
except re.error as err: except re.error as err:
raise ValueError(f"Wildcard error in {pair_wc}, {err}") raise ValueError(f"Wildcard error in {pair_wc}, {err}")
# Remove wildcard pairs that didn't have a match.
result = [element for element in result if re.fullmatch(r"^[A-Za-z0-9:/-]+$", element)] result = [element for element in result if re.fullmatch(r"^[A-Za-z0-9:/-]+$", element)]
else: else:

View File

@ -255,7 +255,6 @@ def test_init_exception(default_conf, mocker):
def test_exchange_resolver(default_conf, mocker, caplog): def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=MagicMock())) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=MagicMock()))
mocker.patch(f"{EXMS}._load_async_markets") mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@ -555,7 +554,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
def test__load_async_markets(default_conf, mocker, caplog): def test__load_async_markets(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt") mocker.patch(f"{EXMS}._init_ccxt")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -584,7 +582,6 @@ def test__load_markets(default_conf, mocker, caplog):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = get_mock_coro(side_effect=ccxt.BaseError("SomeError")) api_mock.load_markets = get_mock_coro(side_effect=ccxt.BaseError("SomeError"))
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@ -684,7 +681,6 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
} }
) )
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf) Exchange(default_conf)
@ -702,7 +698,6 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
} }
) )
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
with pytest.raises( with pytest.raises(
ConfigurationError, ConfigurationError,
@ -755,147 +750,6 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
assert ex.get_pair_base_currency(pair) == expected assert ex.get_pair_base_currency(pair) == expected
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
id_mock = PropertyMock(return_value="test_exchange")
type(api_mock).id = id_mock
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(
f"{EXMS}._load_async_markets",
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
},
)
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
# test exchange.validate_pairs directly
# No assert - but this should not fail (!)
Exchange(default_conf)
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(
return_value={"XRP/BTC": {"inactive": True, "base": "XRP", "quote": "BTC"}}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}._load_async_markets")
with pytest.raises(OperationalException, match=r"not available"):
Exchange(default_conf)
def test_validate_pairs_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
mocker.patch(f"{EXMS}.name", PropertyMock(return_value="Binance"))
type(api_mock).markets = PropertyMock(return_value={})
mocker.patch(f"{EXMS}._init_ccxt", api_mock)
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}._load_async_markets")
with pytest.raises(OperationalException, match=r"Pair ETH/BTC is not available on Binance"):
Exchange(default_conf)
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value={}))
Exchange(default_conf)
assert log_has("Unable to validate pairs (assuming they are correct).", caplog)
def test_validate_pairs_restricted(default_conf, mocker, caplog):
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC", "info": {"prohibitedIn": ["US"]}},
"NEO/BTC": {"quote": "BTC", "info": "TestString"}, # info can also be a string ...
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency")
Exchange(default_conf)
assert log_has(
"Pair XRP/BTC is restricted for some users on this exchange."
"Please check if you are impacted by this restriction "
"on the exchange and eventually remove XRP/BTC from your whitelist.",
caplog,
)
def test_validate_pairs_stakecompatibility(default_conf, mocker):
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "BTC"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker):
api_mock = MagicMock()
default_conf["stake_currency"] = ""
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "BTC"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf)
assert type(api_mock).load_markets.call_count == 1
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker):
default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD")
api_mock = MagicMock()
type(api_mock).load_markets = get_mock_coro(
return_value={
"ETH/BTC": {"quote": "BTC"},
"LTC/BTC": {"quote": "BTC"},
"XRP/BTC": {"quote": "BTC"},
"NEO/BTC": {"quote": "BTC"},
"HELLO-WORLD": {"quote": "USDT"},
}
)
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
Exchange(default_conf)
@pytest.mark.parametrize("timeframe", [("5m"), ("1m"), ("15m"), ("1h")]) @pytest.mark.parametrize("timeframe", [("5m"), ("1m"), ("15m"), ("1h")])
def test_validate_timeframes(default_conf, mocker, timeframe): def test_validate_timeframes(default_conf, mocker, timeframe):
default_conf["timeframe"] = timeframe default_conf["timeframe"] = timeframe
@ -907,7 +761,6 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
Exchange(default_conf) Exchange(default_conf)
@ -925,7 +778,6 @@ def test_validate_timeframes_failed(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
with pytest.raises( with pytest.raises(
@ -955,7 +807,6 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
@ -977,7 +828,6 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs", MagicMock())
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
@ -999,7 +849,6 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_required_startup_candles") mocker.patch(f"{EXMS}.validate_required_startup_candles")
@ -1016,7 +865,6 @@ def test_validate_pricing(default_conf, mocker):
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode") mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.name", "Binance") mocker.patch(f"{EXMS}.name", "Binance")
@ -1051,7 +899,6 @@ def test_validate_ordertypes(default_conf, mocker):
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True}) type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@ -1110,7 +957,6 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True}) type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
@ -1135,7 +981,6 @@ def test_validate_order_types_not_in_config(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock)) mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
mocker.patch(f"{EXMS}.reload_markets") mocker.patch(f"{EXMS}.reload_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -1151,7 +996,6 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
mocker.patch(f"{EXMS}._init_ccxt", api_mock) mocker.patch(f"{EXMS}._init_ccxt", api_mock)
mocker.patch(f"{EXMS}.validate_timeframes") mocker.patch(f"{EXMS}.validate_timeframes")
mocker.patch(f"{EXMS}._load_async_markets") mocker.patch(f"{EXMS}._load_async_markets")
mocker.patch(f"{EXMS}.validate_pairs")
mocker.patch(f"{EXMS}.validate_pricing") mocker.patch(f"{EXMS}.validate_pricing")
mocker.patch(f"{EXMS}.validate_stakecurrency") mocker.patch(f"{EXMS}.validate_stakecurrency")
@ -4185,7 +4029,6 @@ def test_merge_ft_has_dict(default_conf, mocker):
EXMS, EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()), _init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(), _load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(), validate_timeframes=MagicMock(),
validate_stakecurrency=MagicMock(), validate_stakecurrency=MagicMock(),
validate_pricing=MagicMock(), validate_pricing=MagicMock(),
@ -4220,7 +4063,6 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
EXMS, EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()), _init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(), _load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(), validate_timeframes=MagicMock(),
validate_pricing=MagicMock(), validate_pricing=MagicMock(),
markets=PropertyMock(return_value=markets), markets=PropertyMock(return_value=markets),
@ -4500,7 +4342,6 @@ def test_get_markets(
EXMS, EXMS,
_init_ccxt=MagicMock(return_value=MagicMock()), _init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(), _load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(), validate_timeframes=MagicMock(),
validate_pricing=MagicMock(), validate_pricing=MagicMock(),
markets=PropertyMock(return_value=markets_static), markets=PropertyMock(return_value=markets_static),

View File

@ -2204,7 +2204,6 @@ def test_manage_open_orders_buy_exception(
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
EXMS, EXMS,
validate_pairs=MagicMock(),
fetch_ticker=ticker_usdt, fetch_ticker=ticker_usdt,
fetch_order=MagicMock(side_effect=ExchangeError), fetch_order=MagicMock(side_effect=ExchangeError),
cancel_order=cancel_order_mock, cancel_order=cancel_order_mock,

View File

@ -13,6 +13,12 @@ from freqtrade.optimize.analysis.lookahead_helpers import LookaheadAnalysisSubFu
from tests.conftest import EXMS, get_args, log_has_re, patch_exchange from tests.conftest import EXMS, get_args, log_has_re, patch_exchange
IGNORE_BIASED_INDICATORS_CAPTION = (
"Any indicators in 'biased_indicators' which are used within "
"set_freqai_targets() can be ignored."
)
@pytest.fixture @pytest.fixture
def lookahead_conf(default_conf_usdt, tmp_path): def lookahead_conf(default_conf_usdt, tmp_path):
default_conf_usdt["user_data_dir"] = tmp_path default_conf_usdt["user_data_dir"] = tmp_path
@ -133,6 +139,58 @@ def test_lookahead_helper_start(lookahead_conf, mocker) -> None:
text_table_mock.reset_mock() text_table_mock.reset_mock()
@pytest.mark.parametrize(
"indicators, expected_caption_text",
[
(
["&indicator1", "indicator2"],
IGNORE_BIASED_INDICATORS_CAPTION,
),
(
["indicator1", "&indicator2"],
IGNORE_BIASED_INDICATORS_CAPTION,
),
(
["&indicator1", "&indicator2"],
IGNORE_BIASED_INDICATORS_CAPTION,
),
(["indicator1", "indicator2"], None),
([], None),
],
ids=(
"First of two biased indicators starts with '&'",
"Second of two biased indicators starts with '&'",
"Both biased indicators start with '&'",
"No biased indicators start with '&'",
"Empty biased indicators list",
),
)
def test_lookahead_helper_start__caption_based_on_indicators(
indicators, expected_caption_text, lookahead_conf, mocker
):
"""Test that the table caption is only populated if a biased_indicator starts with '&'."""
single_mock = MagicMock()
lookahead_analysis = LookaheadAnalysis(
lookahead_conf,
{"name": "strategy_test_v3_with_lookahead_bias"},
)
lookahead_analysis.current_analysis.false_indicators = indicators
single_mock.return_value = lookahead_analysis
text_table_mock = MagicMock()
mocker.patch.multiple(
"freqtrade.optimize.analysis.lookahead_helpers.LookaheadAnalysisSubFunctions",
initialize_single_lookahead_analysis=single_mock,
text_table_lookahead_analysis_instances=text_table_mock,
)
LookaheadAnalysisSubFunctions.start(lookahead_conf)
text_table_mock.assert_called_once_with(
lookahead_conf, [lookahead_analysis], caption=expected_caption_text
)
def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf): def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf):
analysis = Analysis() analysis = Analysis()
analysis.has_bias = True analysis.has_bias = True
@ -199,6 +257,53 @@ def test_lookahead_helper_text_table_lookahead_analysis_instances(lookahead_conf
assert len(data) == 3 assert len(data) == 3
@pytest.mark.parametrize(
"caption",
[
"",
"A test caption",
None,
False,
],
ids=(
"Pass empty string",
"Pass non-empty string",
"Pass None",
"Don't pass caption",
),
)
def test_lookahead_helper_text_table_lookahead_analysis_instances__caption(
caption,
lookahead_conf,
mocker,
):
"""Test that the caption is passed in the table kwargs when calling print_rich_table()."""
print_rich_table_mock = MagicMock()
mocker.patch(
"freqtrade.optimize.analysis.lookahead_helpers.print_rich_table",
print_rich_table_mock,
)
lookahead_analysis = LookaheadAnalysis(
lookahead_conf,
{
"name": "strategy_test_v3_with_lookahead_bias",
"location": Path(lookahead_conf["strategy_path"], f"{lookahead_conf['strategy']}.py"),
},
)
kwargs = {}
if caption is not False:
kwargs["caption"] = caption
LookaheadAnalysisSubFunctions.text_table_lookahead_analysis_instances(
lookahead_conf, [lookahead_analysis], **kwargs
)
assert print_rich_table_mock.call_args[-1]["table_kwargs"]["caption"] == (
caption if caption is not False else None
)
def test_lookahead_helper_export_to_csv(lookahead_conf): def test_lookahead_helper_export_to_csv(lookahead_conf):
import pandas as pd import pandas as pd