Merge branch 'freqtrade:develop' into fix-docs
This commit is contained in:
commit
b6ad0f52e9
@ -9,7 +9,9 @@
|
|||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30
|
"sell": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0,
|
"ask_last_balance": 0.0,
|
||||||
|
@ -9,7 +9,9 @@
|
|||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30
|
"sell": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
|
@ -9,7 +9,9 @@
|
|||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30
|
"sell": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0,
|
"ask_last_balance": 0.0,
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
"stoploss": -0.10,
|
"stoploss": -0.10,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30,
|
"sell": 10,
|
||||||
"exit_timeout_count": 0,
|
"exit_timeout_count": 0,
|
||||||
"unit": "minutes"
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
|
@ -9,7 +9,9 @@
|
|||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30
|
"sell": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
|
@ -76,6 +76,7 @@ optional arguments:
|
|||||||
_today.json`
|
_today.json`
|
||||||
--breakdown {day,week,month} [{day,week,month} ...]
|
--breakdown {day,week,month} [{day,week,month} ...]
|
||||||
Show backtesting breakdown per [day, week, month].
|
Show backtesting breakdown per [day, week, month].
|
||||||
|
--no-cache Do not reuse cached backtest results.
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
@ -457,6 +458,14 @@ freqtrade backtesting --strategy MyAwesomeStrategy --breakdown day month
|
|||||||
|
|
||||||
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day.
|
The output will show a table containing the realized absolute Profit (in stake currency) for the given timeperiod, as well as wins, draws and losses that materialized (closed) on this day.
|
||||||
|
|
||||||
|
### Backtest result caching
|
||||||
|
|
||||||
|
To save time, by default backtest will reuse a cached result when backtested strategy and config match that of previous backtest. To force a new backtest despite existing result for identical run specify `--no-cache` parameter.
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Caching is automatically disabled for open-ended timeranges (`--timerange 20210101-`), as freqtrade cannot ensure reliably that the underlying data didn't change. It can also use cached results where it shouldn't if the original backtest had missing data at the end, which was fixed by downloading more data.
|
||||||
|
In this instance, please use `--no-cache` once to get a fresh backtest.
|
||||||
|
|
||||||
### Further backtest-result analysis
|
### Further backtest-result analysis
|
||||||
|
|
||||||
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
|
||||||
|
@ -24,7 +24,7 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
|
|||||||
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
|
||||||
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
"enable_protections", "dry_run_wallet", "timeframe_detail",
|
||||||
"strategy_list", "export", "exportfilename",
|
"strategy_list", "export", "exportfilename",
|
||||||
"backtest_breakdown"]
|
"backtest_breakdown", "no_backtest_cache"]
|
||||||
|
|
||||||
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
||||||
"position_stacking", "use_max_market_positions",
|
"position_stacking", "use_max_market_positions",
|
||||||
|
@ -205,6 +205,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
nargs='+',
|
nargs='+',
|
||||||
choices=constants.BACKTEST_BREAKDOWNS
|
choices=constants.BACKTEST_BREAKDOWNS
|
||||||
),
|
),
|
||||||
|
"no_backtest_cache": Arg(
|
||||||
|
'--no-cache',
|
||||||
|
help='Do not reuse cached backtest results.',
|
||||||
|
action='store_true'
|
||||||
|
),
|
||||||
# Edge
|
# Edge
|
||||||
"stoploss_range": Arg(
|
"stoploss_range": Arg(
|
||||||
'--stoplosses',
|
'--stoplosses',
|
||||||
|
@ -276,6 +276,9 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='backtest_breakdown',
|
self._args_to_config(config, argname='backtest_breakdown',
|
||||||
logstring='Parameter --breakdown detected ...')
|
logstring='Parameter --breakdown detected ...')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='no_backtest_cache',
|
||||||
|
logstring='Parameter --no-cache detected ...')
|
||||||
|
|
||||||
self._args_to_config(config, argname='disableparamexport',
|
self._args_to_config(config, argname='disableparamexport',
|
||||||
logstring='Parameter --disableparamexport detected: {} ...')
|
logstring='Parameter --disableparamexport detected: {} ...')
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
Helpers when analyzing backtest data
|
Helpers when analyzing backtest data
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from copy import copy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from freqtrade.constants import LAST_BT_RESULT_FN
|
from freqtrade.constants import LAST_BT_RESULT_FN
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import json_load
|
from freqtrade.misc import get_backtest_metadata_filename, json_load
|
||||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
from freqtrade.persistence import LocalTrade, Trade, init_db
|
||||||
|
|
||||||
|
|
||||||
@ -102,6 +103,23 @@ def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str =
|
|||||||
return directory / get_latest_hyperopt_filename(directory)
|
return directory / get_latest_hyperopt_filename(directory)
|
||||||
|
|
||||||
|
|
||||||
|
def load_backtest_metadata(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Read metadata dictionary from backtest results file without reading and deserializing entire
|
||||||
|
file.
|
||||||
|
:param filename: path to backtest results file.
|
||||||
|
:return: metadata dict or None if metadata is not present.
|
||||||
|
"""
|
||||||
|
filename = get_backtest_metadata_filename(filename)
|
||||||
|
try:
|
||||||
|
with filename.open() as fp:
|
||||||
|
return json_load(fp)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {}
|
||||||
|
except Exception as e:
|
||||||
|
raise OperationalException('Unexpected error while loading backtest metadata.') from e
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Load backtest statistics file.
|
Load backtest statistics file.
|
||||||
@ -118,9 +136,56 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]:
|
|||||||
with filename.open() as file:
|
with filename.open() as file:
|
||||||
data = json_load(file)
|
data = json_load(file)
|
||||||
|
|
||||||
|
# Legacy list format does not contain metadata.
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data['metadata'] = load_backtest_metadata(filename)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def find_existing_backtest_stats(dirname: Union[Path, str],
|
||||||
|
run_ids: Dict[str, str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Find existing backtest stats that match specified run IDs and load them.
|
||||||
|
:param dirname: pathlib.Path object, or string pointing to the file.
|
||||||
|
:param run_ids: {strategy_name: id_string} dictionary.
|
||||||
|
:return: results dict.
|
||||||
|
"""
|
||||||
|
# Copy so we can modify this dict without affecting parent scope.
|
||||||
|
run_ids = copy(run_ids)
|
||||||
|
dirname = Path(dirname)
|
||||||
|
results: Dict[str, Any] = {
|
||||||
|
'metadata': {},
|
||||||
|
'strategy': {},
|
||||||
|
'strategy_comparison': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Weird glob expression here avoids including .meta.json files.
|
||||||
|
for filename in reversed(sorted(dirname.glob('backtest-result-*-[0-9][0-9].json'))):
|
||||||
|
metadata = load_backtest_metadata(filename)
|
||||||
|
if not metadata:
|
||||||
|
# Files are sorted from newest to oldest. When file without metadata is encountered it
|
||||||
|
# is safe to assume older files will also not have any metadata.
|
||||||
|
break
|
||||||
|
|
||||||
|
for strategy_name, run_id in list(run_ids.items()):
|
||||||
|
if metadata.get(strategy_name, {}).get('run_id') == run_id:
|
||||||
|
# TODO: load_backtest_stats() may load an old version of backtest which is
|
||||||
|
# incompatible with current version.
|
||||||
|
del run_ids[strategy_name]
|
||||||
|
bt_data = load_backtest_stats(filename)
|
||||||
|
for k in ('metadata', 'strategy'):
|
||||||
|
results[k][strategy_name] = bt_data[k][strategy_name]
|
||||||
|
comparison = bt_data['strategy_comparison']
|
||||||
|
for i in range(len(comparison)):
|
||||||
|
if comparison[i]['key'] == strategy_name:
|
||||||
|
results['strategy_comparison'].append(comparison[i])
|
||||||
|
break
|
||||||
|
if len(run_ids) == 0:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Load backtest data file.
|
Load backtest data file.
|
||||||
|
@ -7,11 +7,25 @@ from typing import Any, Dict
|
|||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
|
|
||||||
|
|
||||||
|
class FTBufferingHandler(BufferingHandler):
|
||||||
|
def flush(self):
|
||||||
|
"""
|
||||||
|
Override Flush behaviour - we keep half of the configured capacity
|
||||||
|
otherwise, we have moments with "empty" logs.
|
||||||
|
"""
|
||||||
|
self.acquire()
|
||||||
|
try:
|
||||||
|
# Keep half of the records in buffer.
|
||||||
|
self.buffer = self.buffer[-int(self.capacity / 2):]
|
||||||
|
finally:
|
||||||
|
self.release()
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
|
||||||
# Initialize bufferhandler - will be used for /log endpoints
|
# Initialize bufferhandler - will be used for /log endpoints
|
||||||
bufferHandler = BufferingHandler(1000)
|
bufferHandler = FTBufferingHandler(1000)
|
||||||
bufferHandler.setFormatter(Formatter(LOGFORMAT))
|
bufferHandler.setFormatter(Formatter(LOGFORMAT))
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
Various tool function for Freqtrade and scripts
|
Various tool function for Freqtrade and scripts
|
||||||
"""
|
"""
|
||||||
import gzip
|
import gzip
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Iterator, List
|
from typing import Any, Iterator, List, Union
|
||||||
from typing.io import IO
|
from typing.io import IO
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -228,3 +230,32 @@ def parse_db_uri_for_logging(uri: str):
|
|||||||
return uri
|
return uri
|
||||||
pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0]
|
pwd = parsed_db_uri.netloc.split(':')[1].split('@')[0]
|
||||||
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
|
return parsed_db_uri.geturl().replace(f':{pwd}@', ':*****@')
|
||||||
|
|
||||||
|
|
||||||
|
def get_strategy_run_id(strategy) -> str:
|
||||||
|
"""
|
||||||
|
Generate unique identification hash for a backtest run. Identical config and strategy file will
|
||||||
|
always return an identical hash.
|
||||||
|
:param strategy: strategy object.
|
||||||
|
:return: hex string id.
|
||||||
|
"""
|
||||||
|
digest = hashlib.sha1()
|
||||||
|
config = deepcopy(strategy.config)
|
||||||
|
|
||||||
|
# Options that have no impact on results of individual backtest.
|
||||||
|
not_important_keys = ('strategy_list', 'original_config', 'telegram', 'api_server')
|
||||||
|
for k in not_important_keys:
|
||||||
|
if k in config:
|
||||||
|
del config[k]
|
||||||
|
|
||||||
|
digest.update(rapidjson.dumps(config, default=str,
|
||||||
|
number_mode=rapidjson.NM_NATIVE).encode('utf-8'))
|
||||||
|
with open(strategy.__file__, 'rb') as fp:
|
||||||
|
digest.update(fp.read())
|
||||||
|
return digest.hexdigest().lower()
|
||||||
|
|
||||||
|
|
||||||
|
def get_backtest_metadata_filename(filename: Union[Path, str]) -> Path:
|
||||||
|
"""Return metadata filename for specified backtest results file."""
|
||||||
|
filename = Path(filename)
|
||||||
|
return filename.parent / Path(f'{filename.stem}.meta{filename.suffix}')
|
||||||
|
@ -14,12 +14,13 @@ from pandas import DataFrame
|
|||||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.data import history
|
from freqtrade.data import history
|
||||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
|
||||||
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
from freqtrade.data.converter import trim_dataframe, trim_dataframes
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.enums import BacktestState, SellType
|
from freqtrade.enums import BacktestState, SellType
|
||||||
from freqtrade.exceptions import DependencyException, OperationalException
|
from freqtrade.exceptions import DependencyException, OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
|
from freqtrade.misc import get_strategy_run_id
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
from freqtrade.optimize.bt_progress import BTProgress
|
from freqtrade.optimize.bt_progress import BTProgress
|
||||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||||
@ -60,7 +61,7 @@ class Backtesting:
|
|||||||
|
|
||||||
LoggingMixin.show_output = False
|
LoggingMixin.show_output = False
|
||||||
self.config = config
|
self.config = config
|
||||||
self.results: Optional[Dict[str, Any]] = None
|
self.results: Dict[str, Any] = {}
|
||||||
|
|
||||||
config['dry_run'] = True
|
config['dry_run'] = True
|
||||||
self.strategylist: List[IStrategy] = []
|
self.strategylist: List[IStrategy] = []
|
||||||
@ -727,6 +728,7 @@ class Backtesting:
|
|||||||
)
|
)
|
||||||
backtest_end_time = datetime.now(timezone.utc)
|
backtest_end_time = datetime.now(timezone.utc)
|
||||||
results.update({
|
results.update({
|
||||||
|
'run_id': get_strategy_run_id(strat),
|
||||||
'backtest_start_time': int(backtest_start_time.timestamp()),
|
'backtest_start_time': int(backtest_start_time.timestamp()),
|
||||||
'backtest_end_time': int(backtest_end_time.timestamp()),
|
'backtest_end_time': int(backtest_end_time.timestamp()),
|
||||||
})
|
})
|
||||||
@ -745,15 +747,52 @@ class Backtesting:
|
|||||||
self.load_bt_data_detail()
|
self.load_bt_data_detail()
|
||||||
logger.info("Dataload complete. Calculating indicators")
|
logger.info("Dataload complete. Calculating indicators")
|
||||||
|
|
||||||
for strat in self.strategylist:
|
run_ids = {
|
||||||
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
|
strategy.get_strategy_name(): get_strategy_run_id(strategy)
|
||||||
if len(self.strategylist) > 0:
|
for strategy in self.strategylist
|
||||||
|
}
|
||||||
|
|
||||||
self.results = generate_backtest_stats(data, self.all_results,
|
# Load previous result that will be updated incrementally.
|
||||||
min_date=min_date, max_date=max_date)
|
# This can be circumvented in certain instances in combination with downloading more data
|
||||||
|
if self.timerange.stopts == 0 or datetime.fromtimestamp(
|
||||||
|
self.timerange.stopts, tz=timezone.utc) > datetime.now(tz=timezone.utc):
|
||||||
|
self.config['no_backtest_cache'] = True
|
||||||
|
logger.warning('Backtest result caching disabled due to use of open-ended timerange.')
|
||||||
|
|
||||||
|
if not self.config.get('no_backtest_cache', False):
|
||||||
|
self.results = find_existing_backtest_stats(
|
||||||
|
self.config['user_data_dir'] / 'backtest_results', run_ids)
|
||||||
|
|
||||||
|
for strat in self.strategylist:
|
||||||
|
if self.results and strat.get_strategy_name() in self.results['strategy']:
|
||||||
|
# When previous result hash matches - reuse that result and skip backtesting.
|
||||||
|
logger.info(f'Reusing result of previous backtest for {strat.get_strategy_name()}')
|
||||||
|
continue
|
||||||
|
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
|
||||||
|
|
||||||
|
# Update old results with new ones.
|
||||||
|
if len(self.all_results) > 0:
|
||||||
|
results = generate_backtest_stats(
|
||||||
|
data, self.all_results, min_date=min_date, max_date=max_date)
|
||||||
|
if self.results:
|
||||||
|
self.results['metadata'].update(results['metadata'])
|
||||||
|
self.results['strategy'].update(results['strategy'])
|
||||||
|
self.results['strategy_comparison'].extend(results['strategy_comparison'])
|
||||||
|
else:
|
||||||
|
self.results = results
|
||||||
|
|
||||||
if self.config.get('export', 'none') == 'trades':
|
if self.config.get('export', 'none') == 'trades':
|
||||||
store_backtest_stats(self.config['exportfilename'], self.results)
|
store_backtest_stats(self.config['exportfilename'], self.results)
|
||||||
|
|
||||||
|
# Results may be mixed up now. Sort them so they follow --strategy-list order.
|
||||||
|
if 'strategy_list' in self.config and len(self.results) > 0:
|
||||||
|
self.results['strategy_comparison'] = sorted(
|
||||||
|
self.results['strategy_comparison'],
|
||||||
|
key=lambda c: self.config['strategy_list'].index(c['key']))
|
||||||
|
self.results['strategy'] = dict(
|
||||||
|
sorted(self.results['strategy'].items(),
|
||||||
|
key=lambda kv: self.config['strategy_list'].index(kv[0])))
|
||||||
|
|
||||||
|
if len(self.strategylist) > 0:
|
||||||
# Show backtest results
|
# Show backtest results
|
||||||
show_backtest_results(self.config, self.results)
|
show_backtest_results(self.config, self.results)
|
||||||
|
@ -34,7 +34,7 @@ class EdgeCli:
|
|||||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||||
self.strategy.dp = DataProvider(config, None)
|
self.strategy.dp = DataProvider(config, self.exchange)
|
||||||
|
|
||||||
validate_config_consistency(self.config)
|
validate_config_consistency(self.config)
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ from tabulate import tabulate
|
|||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
|
||||||
from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
|
from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
|
||||||
calculate_max_drawdown)
|
calculate_max_drawdown)
|
||||||
from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value
|
from freqtrade.misc import (decimals_per_coin, file_dump_json, get_backtest_metadata_filename,
|
||||||
|
round_coin_value)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -33,6 +34,11 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N
|
|||||||
recordfilename.parent,
|
recordfilename.parent,
|
||||||
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
|
f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}'
|
||||||
).with_suffix(recordfilename.suffix)
|
).with_suffix(recordfilename.suffix)
|
||||||
|
|
||||||
|
# Store metadata separately.
|
||||||
|
file_dump_json(get_backtest_metadata_filename(filename), stats['metadata'])
|
||||||
|
del stats['metadata']
|
||||||
|
|
||||||
file_dump_json(filename, stats)
|
file_dump_json(filename, stats)
|
||||||
|
|
||||||
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
|
latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN)
|
||||||
@ -509,16 +515,25 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
|
|||||||
:param max_date: Backtest end date
|
:param max_date: Backtest end date
|
||||||
:return: Dictionary containing results per strategy and a strategy summary.
|
:return: Dictionary containing results per strategy and a strategy summary.
|
||||||
"""
|
"""
|
||||||
result: Dict[str, Any] = {'strategy': {}}
|
result: Dict[str, Any] = {
|
||||||
|
'metadata': {},
|
||||||
|
'strategy': {},
|
||||||
|
'strategy_comparison': [],
|
||||||
|
}
|
||||||
market_change = calculate_market_change(btdata, 'close')
|
market_change = calculate_market_change(btdata, 'close')
|
||||||
|
metadata = {}
|
||||||
pairlist = list(btdata.keys())
|
pairlist = list(btdata.keys())
|
||||||
for strategy, content in all_results.items():
|
for strategy, content in all_results.items():
|
||||||
strat_stats = generate_strategy_stats(pairlist, strategy, content,
|
strat_stats = generate_strategy_stats(pairlist, strategy, content,
|
||||||
min_date, max_date, market_change=market_change)
|
min_date, max_date, market_change=market_change)
|
||||||
|
metadata[strategy] = {
|
||||||
|
'run_id': content['run_id']
|
||||||
|
}
|
||||||
result['strategy'][strategy] = strat_stats
|
result['strategy'][strategy] = strat_stats
|
||||||
|
|
||||||
strategy_results = generate_strategy_comparison(bt_stats=result['strategy'])
|
strategy_results = generate_strategy_comparison(bt_stats=result['strategy'])
|
||||||
|
|
||||||
|
result['metadata'] = metadata
|
||||||
result['strategy_comparison'] = strategy_results
|
result['strategy_comparison'] = strategy_results
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -592,6 +592,7 @@ class RPC:
|
|||||||
value = self._fiat_converter.convert_amount(
|
value = self._fiat_converter.convert_amount(
|
||||||
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||||
|
|
||||||
|
trade_count = len(Trade.get_trades_proxy())
|
||||||
starting_capital_ratio = 0.0
|
starting_capital_ratio = 0.0
|
||||||
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
||||||
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||||
@ -608,6 +609,7 @@ class RPC:
|
|||||||
'starting_capital_fiat': starting_cap_fiat,
|
'starting_capital_fiat': starting_cap_fiat,
|
||||||
'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
|
'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
|
||||||
'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
|
'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
|
||||||
|
'trade_count': trade_count,
|
||||||
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -765,14 +765,17 @@ class Telegram(RPCHandler):
|
|||||||
f"(< {balance_dust_level} {result['stake']}):*\n"
|
f"(< {balance_dust_level} {result['stake']}):*\n"
|
||||||
f"\t`Est. {result['stake']}: "
|
f"\t`Est. {result['stake']}: "
|
||||||
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
||||||
|
tc = result['trade_count'] > 0
|
||||||
|
stake_improve = f" `({result['starting_capital_ratio']:.2%})`" if tc else ''
|
||||||
|
fiat_val = f" `({result['starting_capital_fiat_ratio']:.2%})`" if tc else ''
|
||||||
|
|
||||||
output += ("\n*Estimated Value*:\n"
|
output += ("\n*Estimated Value*:\n"
|
||||||
f"\t`{result['stake']}: "
|
f"\t`{result['stake']}: "
|
||||||
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
||||||
f" `({result['starting_capital_ratio']:.2%})`\n"
|
f"{stake_improve}\n"
|
||||||
f"\t`{result['symbol']}: "
|
f"\t`{result['symbol']}: "
|
||||||
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
||||||
f" `({result['starting_capital_fiat_ratio']:.2%})`\n")
|
f"{fiat_val}\n")
|
||||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
|
@ -654,6 +654,9 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
buy_tag = latest.get(SignalTagType.BUY_TAG.value, None)
|
buy_tag = latest.get(SignalTagType.BUY_TAG.value, None)
|
||||||
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
|
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
|
||||||
|
# Tags can be None, which does not resolve to False.
|
||||||
|
buy_tag = buy_tag if isinstance(buy_tag, str) else None
|
||||||
|
exit_tag = exit_tag if isinstance(exit_tag, str) else None
|
||||||
|
|
||||||
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
|
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
|
||||||
latest['date'], pair, str(buy), str(sell))
|
latest['date'], pair, str(buy), str(sell))
|
||||||
|
@ -15,7 +15,8 @@
|
|||||||
"cancel_open_orders_on_exit": false,
|
"cancel_open_orders_on_exit": false,
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"buy": 10,
|
"buy": 10,
|
||||||
"sell": 30,
|
"sell": 10,
|
||||||
|
"exit_timeout_count": 0,
|
||||||
"unit": "minutes"
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
|
@ -13,7 +13,8 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelis
|
|||||||
calculate_underwater, combine_dataframes_with_mean,
|
calculate_underwater, combine_dataframes_with_mean,
|
||||||
create_cum_profit, extract_trades_of_period,
|
create_cum_profit, extract_trades_of_period,
|
||||||
get_latest_backtest_filename, get_latest_hyperopt_file,
|
get_latest_backtest_filename, get_latest_hyperopt_file,
|
||||||
load_backtest_data, load_trades, load_trades_from_db)
|
load_backtest_data, load_backtest_metadata, load_trades,
|
||||||
|
load_trades_from_db)
|
||||||
from freqtrade.data.history import load_data, load_pair_history
|
from freqtrade.data.history import load_data, load_pair_history
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from tests.conftest import create_mock_trades
|
from tests.conftest import create_mock_trades
|
||||||
@ -40,7 +41,7 @@ def test_get_latest_backtest_filename(testdatadir, mocker):
|
|||||||
get_latest_backtest_filename(testdatadir)
|
get_latest_backtest_filename(testdatadir)
|
||||||
|
|
||||||
|
|
||||||
def test_get_latest_hyperopt_file(testdatadir, mocker):
|
def test_get_latest_hyperopt_file(testdatadir):
|
||||||
res = get_latest_hyperopt_file(testdatadir / 'does_not_exist', 'testfile.pickle')
|
res = get_latest_hyperopt_file(testdatadir / 'does_not_exist', 'testfile.pickle')
|
||||||
assert res == testdatadir / 'does_not_exist/testfile.pickle'
|
assert res == testdatadir / 'does_not_exist/testfile.pickle'
|
||||||
|
|
||||||
@ -51,6 +52,17 @@ def test_get_latest_hyperopt_file(testdatadir, mocker):
|
|||||||
assert res == testdatadir.parent / "hyperopt_results.pickle"
|
assert res == testdatadir.parent / "hyperopt_results.pickle"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_backtest_metadata(mocker, testdatadir):
|
||||||
|
res = load_backtest_metadata(testdatadir / 'nonexistant.file.json')
|
||||||
|
assert res == {}
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.data.btanalysis.get_backtest_metadata_filename')
|
||||||
|
mocker.patch('freqtrade.data.btanalysis.json_load', side_effect=Exception())
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r"Unexpected error.*loading backtest metadata\."):
|
||||||
|
load_backtest_metadata(testdatadir / 'nonexistant.file.json')
|
||||||
|
|
||||||
|
|
||||||
def test_load_backtest_data_old_format(testdatadir, mocker):
|
def test_load_backtest_data_old_format(testdatadir, mocker):
|
||||||
|
|
||||||
filename = testdatadir / "backtest-result_test222.json"
|
filename = testdatadir / "backtest-result_test222.json"
|
||||||
|
@ -1239,3 +1239,86 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
|
|||||||
assert 'BACKTESTING REPORT' in captured.out
|
assert 'BACKTESTING REPORT' in captured.out
|
||||||
assert 'SELL REASON STATS' in captured.out
|
assert 'SELL REASON STATS' in captured.out
|
||||||
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
assert 'LEFT OPEN TRADES REPORT' in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
|
def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testdatadir):
|
||||||
|
|
||||||
|
default_conf.update({
|
||||||
|
"use_sell_signal": True,
|
||||||
|
"sell_profit_only": False,
|
||||||
|
"sell_profit_offset": 0.0,
|
||||||
|
"ignore_roi_if_buy_signal": False,
|
||||||
|
})
|
||||||
|
patch_exchange(mocker)
|
||||||
|
backtestmock = MagicMock(return_value={
|
||||||
|
'results': pd.DataFrame(columns=BT_DATA_COLUMNS),
|
||||||
|
'config': default_conf,
|
||||||
|
'locks': [],
|
||||||
|
'rejected_signals': 20,
|
||||||
|
'final_balance': 1000,
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
|
PropertyMock(return_value=['UNITTEST/BTC']))
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.show_backtest_results', MagicMock())
|
||||||
|
|
||||||
|
load_backtest_metadata = MagicMock(return_value={
|
||||||
|
'StrategyTestV2': {'run_id': '1'},
|
||||||
|
'TestStrategyLegacyV1': {'run_id': 'changed'}
|
||||||
|
})
|
||||||
|
load_backtest_stats = MagicMock(side_effect=[
|
||||||
|
{
|
||||||
|
'metadata': {'StrategyTestV2': {'run_id': '1'}},
|
||||||
|
'strategy': {'StrategyTestV2': {}},
|
||||||
|
'strategy_comparison': [{'key': 'StrategyTestV2'}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'metadata': {'TestStrategyLegacyV1': {'run_id': '2'}},
|
||||||
|
'strategy': {'TestStrategyLegacyV1': {}},
|
||||||
|
'strategy_comparison': [{'key': 'TestStrategyLegacyV1'}]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
mocker.patch('pathlib.Path.glob', return_value=['not important'])
|
||||||
|
mocker.patch.multiple('freqtrade.data.btanalysis',
|
||||||
|
load_backtest_metadata=load_backtest_metadata,
|
||||||
|
load_backtest_stats=load_backtest_stats)
|
||||||
|
mocker.patch('freqtrade.optimize.backtesting.get_strategy_run_id', side_effect=['1', '2', '2'])
|
||||||
|
|
||||||
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'backtesting',
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--datadir', str(testdatadir),
|
||||||
|
'--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'),
|
||||||
|
'--timeframe', '1m',
|
||||||
|
'--timerange', '1510694220-1510700340',
|
||||||
|
'--enable-position-stacking',
|
||||||
|
'--disable-max-market-positions',
|
||||||
|
'--strategy-list',
|
||||||
|
'StrategyTestV2',
|
||||||
|
'TestStrategyLegacyV1',
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
start_backtesting(args)
|
||||||
|
# 1 backtest, 1 loaded from cache
|
||||||
|
assert backtestmock.call_count == 1
|
||||||
|
|
||||||
|
# check the logs, that will contain the backtest result
|
||||||
|
exists = [
|
||||||
|
'Parameter -i/--timeframe detected ... Using timeframe: 1m ...',
|
||||||
|
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
|
||||||
|
'Parameter --timerange detected: 1510694220-1510700340 ...',
|
||||||
|
f'Using data directory: {testdatadir} ...',
|
||||||
|
'Loading data from 2017-11-14 20:57:00 '
|
||||||
|
'up to 2017-11-14 22:58:00 (0 days).',
|
||||||
|
'Backtesting with data from 2017-11-14 21:17:00 '
|
||||||
|
'up to 2017-11-14 22:58:00 (0 days).',
|
||||||
|
'Parameter --enable-position-stacking detected ...',
|
||||||
|
'Reusing result of previous backtest for StrategyTestV2',
|
||||||
|
'Running backtesting for Strategy TestStrategyLegacyV1',
|
||||||
|
]
|
||||||
|
|
||||||
|
for line in exists:
|
||||||
|
assert log_has(line, caplog)
|
||||||
|
@ -84,6 +84,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
|
|||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
||||||
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
||||||
|
'run_id': '123',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timerange = TimeRange.parse_timerange('1510688220-1510700340')
|
timerange = TimeRange.parse_timerange('1510688220-1510700340')
|
||||||
@ -132,6 +133,7 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
|
|||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
||||||
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
||||||
|
'run_id': '124',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,16 +180,16 @@ def test_store_backtest_stats(testdatadir, mocker):
|
|||||||
|
|
||||||
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_json')
|
dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_json')
|
||||||
|
|
||||||
store_backtest_stats(testdatadir, {})
|
store_backtest_stats(testdatadir, {'metadata': {}})
|
||||||
|
|
||||||
assert dump_mock.call_count == 2
|
assert dump_mock.call_count == 3
|
||||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||||
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir/'backtest-result'))
|
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir/'backtest-result'))
|
||||||
|
|
||||||
dump_mock.reset_mock()
|
dump_mock.reset_mock()
|
||||||
filename = testdatadir / 'testresult.json'
|
filename = testdatadir / 'testresult.json'
|
||||||
store_backtest_stats(filename, {})
|
store_backtest_stats(filename, {'metadata': {}})
|
||||||
assert dump_mock.call_count == 2
|
assert dump_mock.call_count == 3
|
||||||
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
assert isinstance(dump_mock.call_args_list[0][0][0], Path)
|
||||||
# result will be testdatadir / testresult-<timestamp>.json
|
# result will be testdatadir / testresult-<timestamp>.json
|
||||||
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult'))
|
assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult'))
|
||||||
|
@ -36,6 +36,10 @@ def test_returns_latest_signal(ohlcv_history):
|
|||||||
mocked_history = ohlcv_history.copy()
|
mocked_history = ohlcv_history.copy()
|
||||||
mocked_history['sell'] = 0
|
mocked_history['sell'] = 0
|
||||||
mocked_history['buy'] = 0
|
mocked_history['buy'] = 0
|
||||||
|
# Set tags in lines that don't matter to test nan in the sell line
|
||||||
|
mocked_history.loc[0, 'buy_tag'] = 'wrong_line'
|
||||||
|
mocked_history.loc[0, 'exit_tag'] = 'wrong_line'
|
||||||
|
|
||||||
mocked_history.loc[1, 'sell'] = 1
|
mocked_history.loc[1, 'sell'] = 1
|
||||||
|
|
||||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None, None)
|
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None, None)
|
||||||
|
@ -22,7 +22,7 @@ from freqtrade.configuration.load_config import load_config_file, load_file, log
|
|||||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
|
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre
|
from freqtrade.loggers import FTBufferingHandler, _set_loggers, setup_logging, setup_logging_pre
|
||||||
from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file
|
from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file
|
||||||
|
|
||||||
|
|
||||||
@ -686,7 +686,7 @@ def test_set_loggers_syslog():
|
|||||||
assert len(logger.handlers) == 3
|
assert len(logger.handlers) == 3
|
||||||
assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler]
|
assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler]
|
||||||
assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
|
assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
|
||||||
assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler]
|
assert [x for x in logger.handlers if type(x) == FTBufferingHandler]
|
||||||
# setting up logging again should NOT cause the loggers to be added a second time.
|
# setting up logging again should NOT cause the loggers to be added a second time.
|
||||||
setup_logging(config)
|
setup_logging(config)
|
||||||
assert len(logger.handlers) == 3
|
assert len(logger.handlers) == 3
|
||||||
@ -709,7 +709,7 @@ def test_set_loggers_Filehandler(tmpdir):
|
|||||||
assert len(logger.handlers) == 3
|
assert len(logger.handlers) == 3
|
||||||
assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler]
|
assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler]
|
||||||
assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
|
assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
|
||||||
assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler]
|
assert [x for x in logger.handlers if type(x) == FTBufferingHandler]
|
||||||
# setting up logging again should NOT cause the loggers to be added a second time.
|
# setting up logging again should NOT cause the loggers to be added a second time.
|
||||||
setup_logging(config)
|
setup_logging(config)
|
||||||
assert len(logger.handlers) == 3
|
assert len(logger.handlers) == 3
|
||||||
|
Loading…
Reference in New Issue
Block a user