Merge branch 'develop' into feat/kevinjulian/add-buy-signal-name
This commit is contained in:
@@ -17,10 +17,11 @@ from freqtrade.data import history
|
||||
from freqtrade.data.btanalysis import trade_list_to_dataframe
|
||||
from freqtrade.data.converter import trim_dataframes
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.enums import SellType
|
||||
from freqtrade.enums import BacktestState, SellType
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||
from freqtrade.mixins import LoggingMixin
|
||||
from freqtrade.optimize.bt_progress import BTProgress
|
||||
from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results,
|
||||
store_backtest_stats)
|
||||
from freqtrade.persistence import LocalTrade, PairLocks, Trade
|
||||
@@ -58,6 +59,7 @@ class Backtesting:
|
||||
|
||||
LoggingMixin.show_output = False
|
||||
self.config = config
|
||||
self.results: Optional[Dict[str, Any]] = None
|
||||
|
||||
# Reset keys for backtesting
|
||||
remove_credentials(self.config)
|
||||
@@ -117,6 +119,10 @@ class Backtesting:
|
||||
|
||||
# Get maximum required startup period
|
||||
self.required_startup = max([strat.startup_candle_count for strat in self.strategylist])
|
||||
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
|
||||
|
||||
self.progress = BTProgress()
|
||||
self.abort = False
|
||||
|
||||
def __del__(self):
|
||||
LoggingMixin.show_output = True
|
||||
@@ -129,6 +135,8 @@ class Backtesting:
|
||||
"""
|
||||
self.strategy: IStrategy = strategy
|
||||
strategy.dp = self.dataprovider
|
||||
# Attach Wallets to Strategy baseclass
|
||||
IStrategy.wallets = self.wallets
|
||||
# Set stoploss_on_exchange to false for backtesting,
|
||||
# since a "perfect" stoploss-sell is assumed anyway
|
||||
# And the regular "stoploss" function would not apply to that case
|
||||
@@ -145,6 +153,8 @@ class Backtesting:
|
||||
Loads backtest data and returns the data combined with the timerange
|
||||
as tuple.
|
||||
"""
|
||||
self.progress.init_step(BacktestState.DATALOAD, 1)
|
||||
|
||||
timerange = TimeRange.parse_timerange(None if self.config.get(
|
||||
'timerange') is None else str(self.config.get('timerange')))
|
||||
|
||||
@@ -168,6 +178,7 @@ class Backtesting:
|
||||
timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe),
|
||||
self.required_startup, min_date)
|
||||
|
||||
self.progress.set_new_value(1)
|
||||
return data, timerange
|
||||
|
||||
def prepare_backtest(self, enable_protections):
|
||||
@@ -182,6 +193,15 @@ class Backtesting:
|
||||
self.rejected_trades = 0
|
||||
self.dataprovider.clear_cache()
|
||||
|
||||
def check_abort(self):
|
||||
"""
|
||||
Check if abort was requested, raise DependencyException if that's the case
|
||||
Only applies to Interactive backtest mode (webserver mode)
|
||||
"""
|
||||
if self.abort:
|
||||
self.abort = False
|
||||
raise DependencyException("Stop requested")
|
||||
|
||||
def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]:
|
||||
"""
|
||||
Helper function to convert a processed dataframes into lists for performance reasons.
|
||||
@@ -192,8 +212,12 @@ class Backtesting:
|
||||
# and eventually change the constants for indexes at the top
|
||||
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_signal_name']
|
||||
data: Dict = {}
|
||||
self.progress.init_step(BacktestState.CONVERT, len(processed))
|
||||
|
||||
# Create dict with data
|
||||
for pair, pair_data in processed.items():
|
||||
self.check_abort()
|
||||
self.progress.increment()
|
||||
if not pair_data.empty:
|
||||
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
|
||||
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
|
||||
@@ -314,7 +338,18 @@ class Backtesting:
|
||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
||||
except DependencyException:
|
||||
return None
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)
|
||||
|
||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) or 0
|
||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||
|
||||
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
|
||||
default_retval=stake_amount)(
|
||||
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
||||
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
|
||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
|
||||
if not stake_amount:
|
||||
return None
|
||||
|
||||
order_type = self.strategy.order_types['buy']
|
||||
time_in_force = self.strategy.order_time_in_force['sell']
|
||||
@@ -407,10 +442,13 @@ class Backtesting:
|
||||
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
|
||||
open_trade_count = 0
|
||||
|
||||
self.progress.init_step(BacktestState.BACKTEST, int(
|
||||
(end_date - start_date) / timedelta(minutes=self.timeframe_min)))
|
||||
|
||||
# Loop timerange and get candle for each pair at that point in time
|
||||
while tmp <= end_date:
|
||||
open_trade_count_start = open_trade_count
|
||||
|
||||
self.check_abort()
|
||||
for i, pair in enumerate(data):
|
||||
row_index = indexes[pair]
|
||||
try:
|
||||
@@ -466,6 +504,7 @@ class Backtesting:
|
||||
self.protections.global_stop(tmp)
|
||||
|
||||
# Move time one configured time_interval ahead.
|
||||
self.progress.increment()
|
||||
tmp += timedelta(minutes=self.timeframe_min)
|
||||
|
||||
trades += self.handle_left_open(open_trades, data=data)
|
||||
@@ -481,6 +520,8 @@ class Backtesting:
|
||||
}
|
||||
|
||||
def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, Any], timerange: TimeRange):
|
||||
self.progress.init_step(BacktestState.ANALYZE, 0)
|
||||
|
||||
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
|
||||
backtest_start_time = datetime.now(timezone.utc)
|
||||
self._set_strategy(strat)
|
||||
@@ -507,6 +548,7 @@ class Backtesting:
|
||||
"No data left after adjusting for startup candles.")
|
||||
|
||||
min_date, max_date = history.get_timerange(preprocessed)
|
||||
|
||||
logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
|
||||
f'({(max_date - min_date).days} days).')
|
||||
@@ -541,11 +583,12 @@ class Backtesting:
|
||||
for strat in self.strategylist:
|
||||
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)
|
||||
if len(self.strategylist) > 0:
|
||||
stats = generate_backtest_stats(data, self.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
||||
self.results = generate_backtest_stats(data, self.all_results,
|
||||
min_date=min_date, max_date=max_date)
|
||||
|
||||
if self.config.get('export', 'none') == 'trades':
|
||||
store_backtest_stats(self.config['exportfilename'], stats)
|
||||
store_backtest_stats(self.config['exportfilename'], self.results)
|
||||
|
||||
# Show backtest results
|
||||
show_backtest_results(self.config, stats)
|
||||
show_backtest_results(self.config, self.results)
|
||||
|
33
freqtrade/optimize/bt_progress.py
Normal file
33
freqtrade/optimize/bt_progress.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from freqtrade.enums import BacktestState
|
||||
|
||||
|
||||
class BTProgress:
|
||||
_action: BacktestState = BacktestState.STARTUP
|
||||
_progress: float = 0
|
||||
_max_steps: float = 0
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def init_step(self, action: BacktestState, max_steps: float):
|
||||
self._action = action
|
||||
self._max_steps = max_steps
|
||||
self._proress = 0
|
||||
|
||||
def set_new_value(self, new_value: float):
|
||||
self._progress = new_value
|
||||
|
||||
def increment(self):
|
||||
self._progress += 1
|
||||
|
||||
@property
|
||||
def progress(self):
|
||||
"""
|
||||
Get progress as ratio, capped to be between 0 and 1 (to avoid small calculation errors).
|
||||
"""
|
||||
return max(min(round(self._progress / self._max_steps, 5)
|
||||
if self._max_steps > 0 else 0, 1), 0)
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
return str(self._action)
|
@@ -12,7 +12,6 @@ from math import ceil
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
import progressbar
|
||||
import rapidjson
|
||||
from colorama import Fore, Style
|
||||
@@ -20,16 +19,16 @@ from colorama import init as colorama_init
|
||||
from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN
|
||||
from freqtrade.data.converter import trim_dataframes
|
||||
from freqtrade.data.history import get_timerange
|
||||
from freqtrade.misc import file_dump_json, plural
|
||||
from freqtrade.misc import deep_merge_dicts, file_dump_json, plural
|
||||
from freqtrade.optimize.backtesting import Backtesting
|
||||
# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
|
||||
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
|
||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools
|
||||
from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer
|
||||
from freqtrade.optimize.optimize_reports import generate_strategy_stats
|
||||
from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver
|
||||
|
||||
@@ -78,8 +77,11 @@ class Hyperopt:
|
||||
|
||||
if not self.config.get('hyperopt'):
|
||||
self.custom_hyperopt = HyperOptAuto(self.config)
|
||||
self.auto_hyperopt = True
|
||||
else:
|
||||
self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config)
|
||||
self.auto_hyperopt = False
|
||||
|
||||
self.backtesting._set_strategy(self.backtesting.strategylist[0])
|
||||
self.custom_hyperopt.strategy = self.backtesting.strategy
|
||||
|
||||
@@ -163,13 +165,9 @@ class Hyperopt:
|
||||
While not a valid json object - this allows appending easily.
|
||||
:param epoch: result dictionary for this epoch.
|
||||
"""
|
||||
def default_parser(x):
|
||||
if isinstance(x, np.integer):
|
||||
return int(x)
|
||||
return str(x)
|
||||
|
||||
epoch[FTHYPT_FILEVERSION] = 2
|
||||
with self.results_file.open('a') as f:
|
||||
rapidjson.dump(epoch, f, default=default_parser,
|
||||
rapidjson.dump(epoch, f, default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN)
|
||||
f.write("\n")
|
||||
|
||||
@@ -201,6 +199,25 @@ class Hyperopt:
|
||||
|
||||
return result
|
||||
|
||||
def _get_no_optimize_details(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get non-optimized parameters
|
||||
"""
|
||||
result: Dict[str, Any] = {}
|
||||
strategy = self.backtesting.strategy
|
||||
if not HyperoptTools.has_space(self.config, 'roi'):
|
||||
result['roi'] = {str(k): v for k, v in strategy.minimal_roi.items()}
|
||||
if not HyperoptTools.has_space(self.config, 'stoploss'):
|
||||
result['stoploss'] = {'stoploss': strategy.stoploss}
|
||||
if not HyperoptTools.has_space(self.config, 'trailing'):
|
||||
result['trailing'] = {
|
||||
'trailing_stop': strategy.trailing_stop,
|
||||
'trailing_stop_positive': strategy.trailing_stop_positive,
|
||||
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
|
||||
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
|
||||
}
|
||||
return result
|
||||
|
||||
def print_results(self, results) -> None:
|
||||
"""
|
||||
Log results if it is better than any previous evaluation
|
||||
@@ -310,7 +327,8 @@ class Hyperopt:
|
||||
results_explanation = HyperoptTools.format_results_explanation_string(
|
||||
strat_stats, self.config['stake_currency'])
|
||||
|
||||
not_optimized = self.backtesting.strategy.get_params_dict()
|
||||
not_optimized = self.backtesting.strategy.get_no_optimize_params()
|
||||
not_optimized = deep_merge_dicts(not_optimized, self._get_no_optimize_details())
|
||||
|
||||
trade_count = strat_stats['total_trades']
|
||||
total_profit = strat_stats['profit_total']
|
||||
@@ -324,7 +342,8 @@ class Hyperopt:
|
||||
loss = self.calculate_loss(results=backtesting_results['results'],
|
||||
trade_count=trade_count,
|
||||
min_date=min_date, max_date=max_date,
|
||||
config=self.config, processed=processed)
|
||||
config=self.config, processed=processed,
|
||||
backtest_stats=strat_stats)
|
||||
return {
|
||||
'loss': loss,
|
||||
'params_dict': params_dict,
|
||||
@@ -469,6 +488,12 @@ class Hyperopt:
|
||||
f"saved to '{self.results_file}'.")
|
||||
|
||||
if self.current_best_epoch:
|
||||
if self.auto_hyperopt:
|
||||
HyperoptTools.try_export_params(
|
||||
self.config,
|
||||
self.backtesting.strategy.get_strategy_name(),
|
||||
self.current_best_epoch)
|
||||
|
||||
HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs,
|
||||
self.print_json)
|
||||
else:
|
||||
|
@@ -5,7 +5,7 @@ This module defines the interface for the loss-function for hyperopt
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from typing import Any, Dict
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
@@ -22,6 +22,7 @@ class IHyperOptLoss(ABC):
|
||||
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||
min_date: datetime, max_date: datetime,
|
||||
config: Dict, processed: Dict[str, DataFrame],
|
||||
backtest_stats: Dict[str, Any],
|
||||
*args, **kwargs) -> float:
|
||||
"""
|
||||
Objective function, returns smaller number for better results
|
||||
|
@@ -1,23 +1,82 @@
|
||||
|
||||
import io
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
import rapidjson
|
||||
import tabulate
|
||||
from colorama import Fore, Style
|
||||
from pandas import isna, json_normalize
|
||||
|
||||
from freqtrade.constants import FTHYPT_FILEVERSION, USERPATH_STRATEGIES
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import round_coin_value, round_dict
|
||||
from freqtrade.misc import deep_merge_dicts, round_coin_value, round_dict, safe_value_fallback2
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NON_OPT_PARAM_APPENDIX = " # value loaded from strategy"
|
||||
|
||||
|
||||
def hyperopt_serializer(x):
|
||||
if isinstance(x, np.integer):
|
||||
return int(x)
|
||||
if isinstance(x, np.bool_):
|
||||
return bool(x)
|
||||
|
||||
return str(x)
|
||||
|
||||
|
||||
class HyperoptTools():
|
||||
|
||||
@staticmethod
|
||||
def get_strategy_filename(config: Dict, strategy_name: str) -> Optional[Path]:
|
||||
"""
|
||||
Get Strategy-location (filename) from strategy_name
|
||||
"""
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||
strategy_objs = StrategyResolver.search_all_objects(directory, False)
|
||||
strategies = [s for s in strategy_objs if s['name'] == strategy_name]
|
||||
if strategies:
|
||||
strategy = strategies[0]
|
||||
|
||||
return Path(strategy['location'])
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def export_params(params, strategy_name: str, filename: Path):
|
||||
"""
|
||||
Generate files
|
||||
"""
|
||||
final_params = deepcopy(params['params_not_optimized'])
|
||||
final_params = deep_merge_dicts(params['params_details'], final_params)
|
||||
final_params = {
|
||||
'strategy_name': strategy_name,
|
||||
'params': final_params,
|
||||
'ft_stratparam_v': 1,
|
||||
'export_time': datetime.now(timezone.utc),
|
||||
}
|
||||
logger.info(f"Dumping parameters to {filename}")
|
||||
rapidjson.dump(final_params, filename.open('w'), indent=2,
|
||||
default=hyperopt_serializer,
|
||||
number_mode=rapidjson.NM_NATIVE | rapidjson.NM_NAN
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def try_export_params(config: Dict[str, Any], strategy_name: str, params: Dict):
|
||||
if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
|
||||
# Export parameters ...
|
||||
fn = HyperoptTools.get_strategy_filename(config, strategy_name)
|
||||
if fn:
|
||||
HyperoptTools.export_params(params, strategy_name, fn.with_suffix('.json'))
|
||||
else:
|
||||
logger.warning("Strategy not found, not exporting parameter file.")
|
||||
|
||||
@staticmethod
|
||||
def has_space(config: Dict[str, Any], space: str) -> bool:
|
||||
"""
|
||||
@@ -99,9 +158,9 @@ class HyperoptTools():
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
|
||||
non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:")
|
||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:")
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
|
||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
|
||||
|
||||
@staticmethod
|
||||
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
||||
@@ -127,23 +186,34 @@ class HyperoptTools():
|
||||
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
|
||||
if space in params or space in non_optimized:
|
||||
space_params = HyperoptTools._space_params(params, space, 5)
|
||||
no_params = HyperoptTools._space_params(non_optimized, space, 5)
|
||||
appendix = ''
|
||||
if not space_params and not no_params:
|
||||
# No parameters - don't print
|
||||
return
|
||||
if not space_params:
|
||||
# Not optimized parameters - append string
|
||||
appendix = NON_OPT_PARAM_APPENDIX
|
||||
|
||||
result = f"\n# {header}\n"
|
||||
if space == 'stoploss':
|
||||
result += f"stoploss = {space_params.get('stoploss')}"
|
||||
elif space == 'roi':
|
||||
if space == "stoploss":
|
||||
stoploss = safe_value_fallback2(space_params, no_params, space, space)
|
||||
result += (f"stoploss = {stoploss}{appendix}")
|
||||
|
||||
elif space == "roi":
|
||||
result = result[:-1] + f'{appendix}\n'
|
||||
minimal_roi_result = rapidjson.dumps({
|
||||
str(k): v for k, v in space_params.items()
|
||||
str(k): v for k, v in (space_params or no_params).items()
|
||||
}, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
|
||||
result += f"minimal_roi = {minimal_roi_result}"
|
||||
elif space == 'trailing':
|
||||
|
||||
for k, v in space_params.items():
|
||||
result += f'{k} = {v}\n'
|
||||
elif space == "trailing":
|
||||
for k, v in (space_params or no_params).items():
|
||||
result += f"{k} = {v}{appendix}\n"
|
||||
|
||||
else:
|
||||
no_params = HyperoptTools._space_params(non_optimized, space, 5)
|
||||
# Buy / sell parameters
|
||||
|
||||
result += f"{space}_params = {HyperoptTools._pprint(space_params, no_params)}"
|
||||
result += f"{space}_params = {HyperoptTools._pprint_dict(space_params, no_params)}"
|
||||
|
||||
result = result.replace("\n", "\n ")
|
||||
print(result)
|
||||
@@ -157,7 +227,7 @@ class HyperoptTools():
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _pprint(params, non_optimized, indent: int = 4):
|
||||
def _pprint_dict(params, non_optimized, indent: int = 4):
|
||||
"""
|
||||
Pretty-print hyperopt results (based on 2 dicts - with add. comment)
|
||||
"""
|
||||
@@ -169,7 +239,7 @@ class HyperoptTools():
|
||||
result += " " * indent + f'"{k}": '
|
||||
result += f'"{param}",' if isinstance(param, str) else f'{param},'
|
||||
if k in non_optimized:
|
||||
result += " # value loaded from strategy"
|
||||
result += NON_OPT_PARAM_APPENDIX
|
||||
result += "\n"
|
||||
result += '}'
|
||||
return result
|
||||
|
@@ -229,8 +229,6 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
winning_trades = results.loc[results['profit_ratio'] > 0]
|
||||
draw_trades = results.loc[results['profit_ratio'] == 0]
|
||||
losing_trades = results.loc[results['profit_ratio'] < 0]
|
||||
zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) &
|
||||
(results['sell_reason'] == 'trailing_stop_loss')])
|
||||
|
||||
holding_avg = (timedelta(minutes=round(results['trade_duration'].mean()))
|
||||
if not results.empty else timedelta())
|
||||
@@ -249,7 +247,6 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
'winner_holding_avg_s': winner_holding_avg.total_seconds(),
|
||||
'loser_holding_avg': loser_holding_avg,
|
||||
'loser_holding_avg_s': loser_holding_avg.total_seconds(),
|
||||
'zero_duration_trades': zero_duration_trades,
|
||||
}
|
||||
|
||||
|
||||
@@ -264,6 +261,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
'winning_days': 0,
|
||||
'draw_days': 0,
|
||||
'losing_days': 0,
|
||||
'daily_profit_list': [],
|
||||
}
|
||||
daily_profit_rel = results.resample('1d', on='close_date')['profit_ratio'].sum()
|
||||
daily_profit = results.resample('1d', on='close_date')['profit_abs'].sum().round(10)
|
||||
@@ -274,6 +272,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
winning_days = sum(daily_profit > 0)
|
||||
draw_days = sum(daily_profit == 0)
|
||||
losing_days = sum(daily_profit < 0)
|
||||
daily_profit_list = [(str(idx.date()), val) for idx, val in daily_profit.iteritems()]
|
||||
|
||||
return {
|
||||
'backtest_best_day': best_rel,
|
||||
@@ -283,6 +282,7 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]:
|
||||
'winning_days': winning_days,
|
||||
'draw_days': draw_days,
|
||||
'losing_days': losing_days,
|
||||
'daily_profit': daily_profit_list,
|
||||
}
|
||||
|
||||
|
||||
@@ -325,8 +325,9 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'],
|
||||
key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None
|
||||
results['open_timestamp'] = results['open_date'].astype(int64) // 1e6
|
||||
results['close_timestamp'] = results['close_date'].astype(int64) // 1e6
|
||||
if not results.empty:
|
||||
results['open_timestamp'] = results['open_date'].view(int64) // 1e6
|
||||
results['close_timestamp'] = results['close_date'].view(int64) // 1e6
|
||||
|
||||
backtest_days = (max_date - min_date).days
|
||||
strat_stats = {
|
||||
@@ -542,14 +543,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
|
||||
# command stores these results and newer version of freqtrade must be able to handle old
|
||||
# results with missing new fields.
|
||||
zero_duration_trades = '--'
|
||||
|
||||
if 'zero_duration_trades' in strat_results:
|
||||
zero_duration_trades_per = \
|
||||
100.0 / strat_results['total_trades'] * strat_results['zero_duration_trades']
|
||||
zero_duration_trades = f'{zero_duration_trades_per:.2f}% ' \
|
||||
f'({strat_results["zero_duration_trades"]})'
|
||||
|
||||
metrics = [
|
||||
('Backtesting from', strat_results['backtest_start']),
|
||||
('Backtesting to', strat_results['backtest_end']),
|
||||
@@ -585,7 +578,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
|
||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
||||
('Zero Duration Trades', zero_duration_trades),
|
||||
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
|
||||
('', ''), # Empty line to improve readability
|
||||
|
||||
|
Reference in New Issue
Block a user