• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 9394559170

26 Apr 2024 06:36AM UTC coverage: 94.656% (-0.02%) from 94.674%
9394559170

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.72
/freqtrade/optimize/hyperopt_tools.py
1
import logging
1✔
2
from copy import deepcopy
1✔
3
from datetime import datetime, timezone
1✔
4
from pathlib import Path
1✔
5
from typing import Any, Dict, Iterator, List, Optional, Tuple
1✔
6

7
import numpy as np
1✔
8
import pandas as pd
1✔
9
import rapidjson
1✔
10
import tabulate
1✔
11
from colorama import Fore, Style
1✔
12
from pandas import isna, json_normalize
1✔
13

14
from freqtrade.constants import FTHYPT_FILEVERSION, Config
1✔
15
from freqtrade.enums import HyperoptState
1✔
16
from freqtrade.exceptions import OperationalException
1✔
17
from freqtrade.misc import deep_merge_dicts, round_dict, safe_value_fallback2
1✔
18
from freqtrade.optimize.hyperopt_epoch_filters import hyperopt_filter_epochs
1✔
19
from freqtrade.optimize.optimize_reports import generate_wins_draws_losses
1✔
20
from freqtrade.util import fmt_coin
1✔
21

22

23
logger = logging.getLogger(__name__)
1✔
24

25
NON_OPT_PARAM_APPENDIX = "  # value loaded from strategy"
1✔
26

27
HYPER_PARAMS_FILE_FORMAT = rapidjson.NM_NATIVE | rapidjson.NM_NAN
1✔
28

29

30
def hyperopt_serializer(x):
1✔
31
    if isinstance(x, np.integer):
1✔
32
        return int(x)
1✔
33
    if isinstance(x, np.bool_):
1✔
34
        return bool(x)
1✔
35

36
    return str(x)
1✔
37

38

39
class HyperoptStateContainer:
1✔
40
    """ Singleton class to track state of hyperopt"""
41
    state: HyperoptState = HyperoptState.OPTIMIZE
1✔
42

43
    @classmethod
1✔
44
    def set_state(cls, value: HyperoptState):
1✔
45
        cls.state = value
1✔
46

47

48
class HyperoptTools:
1✔
49

50
    @staticmethod
1✔
51
    def get_strategy_filename(config: Config, strategy_name: str) -> Optional[Path]:
1✔
52
        """
53
        Get Strategy-location (filename) from strategy_name
54
        """
55
        from freqtrade.resolvers.strategy_resolver import StrategyResolver
1✔
56
        strategy_objs = StrategyResolver.search_all_objects(
1✔
57
            config, False, config.get('recursive_strategy_search', False))
58
        strategies = [s for s in strategy_objs if s['name'] == strategy_name]
1✔
59
        if strategies:
1✔
60
            strategy = strategies[0]
1✔
61

62
            return Path(strategy['location'])
1✔
63
        return None
1✔
64

65
    @staticmethod
1✔
66
    def export_params(params, strategy_name: str, filename: Path):
1✔
67
        """
68
        Generate files
69
        """
70
        final_params = deepcopy(params['params_not_optimized'])
1✔
71
        final_params = deep_merge_dicts(params['params_details'], final_params)
1✔
72
        final_params = {
1✔
73
            'strategy_name': strategy_name,
74
            'params': final_params,
75
            'ft_stratparam_v': 1,
76
            'export_time': datetime.now(timezone.utc),
77
        }
78
        logger.info(f"Dumping parameters to {filename}")
1✔
79
        with filename.open('w') as f:
1✔
80
            rapidjson.dump(final_params, f, indent=2,
1✔
81
                           default=hyperopt_serializer,
82
                           number_mode=HYPER_PARAMS_FILE_FORMAT
83
                           )
84

85
    @staticmethod
1✔
86
    def load_params(filename: Path) -> Dict:
1✔
87
        """
88
        Load parameters from file
89
        """
90
        with filename.open('r') as f:
×
91
            params = rapidjson.load(f, number_mode=HYPER_PARAMS_FILE_FORMAT)
×
92
        return params
×
93

94
    @staticmethod
1✔
95
    def try_export_params(config: Config, strategy_name: str, params: Dict):
1✔
96
        if params.get(FTHYPT_FILEVERSION, 1) >= 2 and not config.get('disableparamexport', False):
1✔
97
            # Export parameters ...
98
            fn = HyperoptTools.get_strategy_filename(config, strategy_name)
1✔
99
            if fn:
1✔
100
                HyperoptTools.export_params(params, strategy_name, fn.with_suffix('.json'))
1✔
101
            else:
102
                logger.warning("Strategy not found, not exporting parameter file.")
1✔
103

104
    @staticmethod
1✔
105
    def has_space(config: Config, space: str) -> bool:
1✔
106
        """
107
        Tell if the space value is contained in the configuration
108
        """
109
        # 'trailing' and 'protection spaces are not included in the 'default' set of spaces
110
        if space in ('trailing', 'protection', 'trades'):
1✔
111
            return any(s in config['spaces'] for s in [space, 'all'])
1✔
112
        else:
113
            return any(s in config['spaces'] for s in [space, 'all', 'default'])
1✔
114

115
    @staticmethod
1✔
116
    def _read_results(results_file: Path, batch_size: int = 10) -> Iterator[List[Any]]:
1✔
117
        """
118
        Stream hyperopt results from file
119
        """
120
        import rapidjson
1✔
121
        logger.info(f"Reading epochs from '{results_file}'")
1✔
122
        with results_file.open('r') as f:
1✔
123
            data = []
1✔
124
            for line in f:
1✔
125
                data += [rapidjson.loads(line)]
1✔
126
                if len(data) >= batch_size:
1✔
127
                    yield data
1✔
128
                    data = []
1✔
129
        yield data
1✔
130

131
    @staticmethod
1✔
132
    def _test_hyperopt_results_exist(results_file) -> bool:
1✔
133
        if results_file.is_file() and results_file.stat().st_size > 0:
1✔
134
            if results_file.suffix == '.pickle':
1✔
135
                raise OperationalException(
1✔
136
                    "Legacy hyperopt results are no longer supported."
137
                    "Please rerun hyperopt or use an older version to load this file."
138
                )
139
            return True
1✔
140
        else:
141
            # No file found.
142
            return False
1✔
143

144
    @staticmethod
1✔
145
    def load_filtered_results(results_file: Path, config: Config) -> Tuple[List, int]:
1✔
146
        filteroptions = {
1✔
147
            'only_best': config.get('hyperopt_list_best', False),
148
            'only_profitable': config.get('hyperopt_list_profitable', False),
149
            'filter_min_trades': config.get('hyperopt_list_min_trades', 0),
150
            'filter_max_trades': config.get('hyperopt_list_max_trades', 0),
151
            'filter_min_avg_time': config.get('hyperopt_list_min_avg_time'),
152
            'filter_max_avg_time': config.get('hyperopt_list_max_avg_time'),
153
            'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit'),
154
            'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit'),
155
            'filter_min_total_profit': config.get('hyperopt_list_min_total_profit'),
156
            'filter_max_total_profit': config.get('hyperopt_list_max_total_profit'),
157
            'filter_min_objective': config.get('hyperopt_list_min_objective'),
158
            'filter_max_objective': config.get('hyperopt_list_max_objective'),
159
        }
160
        if not HyperoptTools._test_hyperopt_results_exist(results_file):
1✔
161
            # No file found.
162
            logger.warning(f"Hyperopt file {results_file} not found.")
1✔
163
            return [], 0
1✔
164

165
        epochs = []
1✔
166
        total_epochs = 0
1✔
167
        for epochs_tmp in HyperoptTools._read_results(results_file):
1✔
168
            if total_epochs == 0 and epochs_tmp[0].get('is_best') is None:
1✔
169
                raise OperationalException(
×
170
                    "The file with HyperoptTools results is incompatible with this version "
171
                    "of Freqtrade and cannot be loaded.")
172
            total_epochs += len(epochs_tmp)
1✔
173
            epochs += hyperopt_filter_epochs(epochs_tmp, filteroptions, log=False)
1✔
174

175
        logger.info(f"Loaded {total_epochs} previous evaluations from disk.")
1✔
176

177
        # Final filter run ...
178
        epochs = hyperopt_filter_epochs(epochs, filteroptions, log=True)
1✔
179

180
        return epochs, total_epochs
1✔
181

182
    @staticmethod
1✔
183
    def show_epoch_details(results, total_epochs: int, print_json: bool,
1✔
184
                           no_header: bool = False, header_str: Optional[str] = None) -> None:
185
        """
186
        Display details of the hyperopt result
187
        """
188
        params = results.get('params_details', {})
1✔
189
        non_optimized = results.get('params_not_optimized', {})
1✔
190

191
        # Default header string
192
        if header_str is None:
1✔
193
            header_str = "Best result"
1✔
194

195
        if not no_header:
1✔
196
            explanation_str = HyperoptTools._format_explanation_string(results, total_epochs)
1✔
197
            print(f"\n{header_str}:\n\n{explanation_str}\n")
1✔
198

199
        if print_json:
1✔
200
            result_dict: Dict = {}
1✔
201
            for s in ['buy', 'sell', 'protection',
1✔
202
                      'roi', 'stoploss', 'trailing', 'max_open_trades']:
203
                HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
1✔
204
            print(rapidjson.dumps(result_dict, default=str, number_mode=HYPER_PARAMS_FILE_FORMAT))
1✔
205

206
        else:
207
            HyperoptTools._params_pretty_print(params, 'buy', "Buy hyperspace params:",
1✔
208
                                               non_optimized)
209
            HyperoptTools._params_pretty_print(params, 'sell', "Sell hyperspace params:",
1✔
210
                                               non_optimized)
211
            HyperoptTools._params_pretty_print(params, 'protection',
1✔
212
                                               "Protection hyperspace params:", non_optimized)
213
            HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
1✔
214
            HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
1✔
215
            HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
1✔
216
            HyperoptTools._params_pretty_print(
1✔
217
                params, 'max_open_trades', "Max Open Trades:", non_optimized)
218

219
    @staticmethod
1✔
220
    def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
1✔
221
        if (space in params) or (space in non_optimized):
1✔
222
            space_params = HyperoptTools._space_params(params, space)
1✔
223
            space_non_optimized = HyperoptTools._space_params(non_optimized, space)
1✔
224
            all_space_params = space_params
1✔
225

226
            # Merge non optimized params if there are any
227
            if len(space_non_optimized) > 0:
1✔
228
                all_space_params = {**space_params, **space_non_optimized}
1✔
229

230
            if space in ['buy', 'sell']:
1✔
231
                result_dict.setdefault('params', {}).update(all_space_params)
1✔
232
            elif space == 'roi':
1✔
233
                # Convert keys in min_roi dict to strings because
234
                # rapidjson cannot dump dicts with integer keys...
235
                result_dict['minimal_roi'] = {str(k): v for k, v in all_space_params.items()}
1✔
236
            else:  # 'stoploss', 'trailing'
237
                result_dict.update(all_space_params)
1✔
238

239
    @staticmethod
1✔
240
    def _params_pretty_print(
1✔
241
            params, space: str, header: str, non_optimized: Optional[Dict] = None) -> None:
242

243
        if space in params or (non_optimized and space in non_optimized):
1✔
244
            space_params = HyperoptTools._space_params(params, space, 5)
1✔
245
            no_params = HyperoptTools._space_params(non_optimized, space, 5)
1✔
246
            appendix = ''
1✔
247
            if not space_params and not no_params:
1✔
248
                # No parameters - don't print
249
                return
×
250
            if not space_params:
1✔
251
                # Not optimized parameters - append string
252
                appendix = NON_OPT_PARAM_APPENDIX
1✔
253

254
            result = f"\n# {header}\n"
1✔
255
            if space == "stoploss":
1✔
256
                stoploss = safe_value_fallback2(space_params, no_params, space, space)
1✔
257
                result += (f"stoploss = {stoploss}{appendix}")
1✔
258
            elif space == "max_open_trades":
1✔
259
                max_open_trades = safe_value_fallback2(space_params, no_params, space, space)
1✔
260
                result += (f"max_open_trades = {max_open_trades}{appendix}")
1✔
261
            elif space == "roi":
1✔
262
                result = result[:-1] + f'{appendix}\n'
1✔
263
                minimal_roi_result = rapidjson.dumps({
1✔
264
                    str(k): v for k, v in (space_params or no_params).items()
265
                }, default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
266
                result += f"minimal_roi = {minimal_roi_result}"
1✔
267
            elif space == "trailing":
1✔
268
                for k, v in (space_params or no_params).items():
1✔
269
                    result += f"{k} = {v}{appendix}\n"
1✔
270

271
            else:
272
                # Buy / sell parameters
273

274
                result += f"{space}_params = {HyperoptTools._pprint_dict(space_params, no_params)}"
1✔
275

276
            result = result.replace("\n", "\n    ")
1✔
277
            print(result)
1✔
278

279
    @staticmethod
1✔
280
    def _space_params(params, space: str, r: Optional[int] = None) -> Dict:
1✔
281
        d = params.get(space)
1✔
282
        if d:
1✔
283
            # Round floats to `r` digits after the decimal point if requested
284
            return round_dict(d, r) if r else d
1✔
285
        return {}
1✔
286

287
    @staticmethod
1✔
288
    def _pprint_dict(params, non_optimized, indent: int = 4):
1✔
289
        """
290
        Pretty-print hyperopt results (based on 2 dicts - with add. comment)
291
        """
292
        p = params.copy()
1✔
293
        p.update(non_optimized)
1✔
294
        result = '{\n'
1✔
295

296
        for k, param in p.items():
1✔
297
            result += " " * indent + f'"{k}": '
1✔
298
            result += f'"{param}",' if isinstance(param, str) else f'{param},'
1✔
299
            if k in non_optimized:
1✔
300
                result += NON_OPT_PARAM_APPENDIX
1✔
301
            result += "\n"
1✔
302
        result += '}'
1✔
303
        return result
1✔
304

305
    @staticmethod
1✔
306
    def is_best_loss(results, current_best_loss: float) -> bool:
1✔
307
        return bool(results['loss'] < current_best_loss)
1✔
308

309
    @staticmethod
1✔
310
    def format_results_explanation_string(results_metrics: Dict, stake_currency: str) -> str:
1✔
311
        """
312
        Return the formatted results explanation in a string
313
        """
314
        return (f"{results_metrics['total_trades']:6d} trades. "
1✔
315
                f"{results_metrics['wins']}/{results_metrics['draws']}"
316
                f"/{results_metrics['losses']} Wins/Draws/Losses. "
317
                f"Avg profit {results_metrics['profit_mean']:7.2%}. "
318
                f"Median profit {results_metrics['profit_median']:7.2%}. "
319
                f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} "
320
                f"({results_metrics['profit_total']:8.2%}). "
321
                f"Avg duration {results_metrics['holding_avg']} min."
322
                )
323

324
    @staticmethod
1✔
325
    def _format_explanation_string(results, total_epochs) -> str:
1✔
326
        return (("*" if results['is_initial_point'] else " ") +
1✔
327
                f"{results['current_epoch']:5d}/{total_epochs}: " +
328
                f"{results['results_explanation']} " +
329
                f"Objective: {results['loss']:.5f}")
330

331
    @staticmethod
1✔
332
    def prepare_trials_columns(trials: pd.DataFrame, has_drawdown: bool) -> pd.DataFrame:
1✔
333
        trials['Best'] = ''
1✔
334

335
        if 'results_metrics.winsdrawslosses' not in trials.columns:
1✔
336
            # Ensure compatibility with older versions of hyperopt results
337
            trials['results_metrics.winsdrawslosses'] = 'N/A'
1✔
338

339
        if not has_drawdown:
1✔
340
            # Ensure compatibility with older versions of hyperopt results
341
            trials['results_metrics.max_drawdown_account'] = None
1✔
342
        if 'is_random' not in trials.columns:
1✔
343
            trials['is_random'] = False
×
344

345
        # New mode, using backtest result for metrics
346
        trials['results_metrics.winsdrawslosses'] = trials.apply(
1✔
347
            lambda x: generate_wins_draws_losses(
348
                            x['results_metrics.wins'], x['results_metrics.draws'],
349
                            x['results_metrics.losses']
350
                      ), axis=1)
351

352
        trials = trials[['Best', 'current_epoch', 'results_metrics.total_trades',
1✔
353
                         'results_metrics.winsdrawslosses',
354
                         'results_metrics.profit_mean', 'results_metrics.profit_total_abs',
355
                         'results_metrics.profit_total', 'results_metrics.holding_avg',
356
                         'results_metrics.max_drawdown',
357
                         'results_metrics.max_drawdown_account', 'results_metrics.max_drawdown_abs',
358
                         'loss', 'is_initial_point', 'is_random', 'is_best']]
359

360
        trials.columns = [
1✔
361
            'Best', 'Epoch', 'Trades', ' Win  Draw  Loss  Win%', 'Avg profit',
362
            'Total profit', 'Profit', 'Avg duration', 'max_drawdown', 'max_drawdown_account',
363
            'max_drawdown_abs', 'Objective', 'is_initial_point', 'is_random', 'is_best'
364
            ]
365

366
        return trials
1✔
367

368
    @staticmethod
1✔
369
    def get_result_table(config: Config, results: list, total_epochs: int, highlight_best: bool,
1✔
370
                         print_colorized: bool, remove_header: int) -> str:
371
        """
372
        Log result table
373
        """
374
        if not results:
1✔
375
            return ''
×
376

377
        tabulate.PRESERVE_WHITESPACE = True
1✔
378
        trials = json_normalize(results, max_level=1)
1✔
379

380
        has_account_drawdown = 'results_metrics.max_drawdown_account' in trials.columns
1✔
381

382
        trials = HyperoptTools.prepare_trials_columns(trials, has_account_drawdown)
1✔
383

384
        trials['is_profit'] = False
1✔
385
        trials.loc[trials['is_initial_point'] | trials['is_random'], 'Best'] = '*     '
1✔
386
        trials.loc[trials['is_best'], 'Best'] = 'Best'
1✔
387
        trials.loc[
1✔
388
            (trials['is_initial_point'] | trials['is_random']) & trials['is_best'],
389
            'Best'] = '* Best'
390
        trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
1✔
391
        trials['Trades'] = trials['Trades'].astype(str)
1✔
392
        # perc_multi = 1 if legacy_mode else 100
393
        trials['Epoch'] = trials['Epoch'].apply(
1✔
394
            lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
395
        )
396
        trials['Avg profit'] = trials['Avg profit'].apply(
1✔
397
            lambda x: f'{x:,.2%}'.rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
398
        )
399
        trials['Avg duration'] = trials['Avg duration'].apply(
1✔
400
            lambda x: f'{x:,.1f} m'.rjust(7, ' ') if isinstance(x, float) else f"{x}"
401
                      if not isna(x) else "--".rjust(7, ' ')
402
        )
403
        trials['Objective'] = trials['Objective'].apply(
1✔
404
            lambda x: f'{x:,.5f}'.rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
405
        )
406

407
        stake_currency = config['stake_currency']
1✔
408

409
        trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply(
1✔
410
            lambda x: "{} {}".format(
411
                fmt_coin(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True),
412
                (f"({x['max_drawdown_account']:,.2%})"
413
                    if has_account_drawdown
414
                    else f"({x['max_drawdown']:,.2%})"
415
                 ).rjust(10, ' ')
416
            ).rjust(25 + len(stake_currency))
417
            if x['max_drawdown'] != 0.0 or x['max_drawdown_account'] != 0.0
418
            else '--'.rjust(25 + len(stake_currency)),
419
            axis=1
420
        )
421

422
        trials = trials.drop(columns=['max_drawdown_abs', 'max_drawdown', 'max_drawdown_account'])
1✔
423

424
        trials['Profit'] = trials.apply(
1✔
425
            lambda x: '{} {}'.format(
426
                fmt_coin(x['Total profit'], stake_currency, keep_trailing_zeros=True),
427
                f"({x['Profit']:,.2%})".rjust(10, ' ')
428
            ).rjust(25 + len(stake_currency))
429
            if x['Total profit'] != 0.0 else '--'.rjust(25 + len(stake_currency)),
430
            axis=1
431
        )
432
        trials = trials.drop(columns=['Total profit'])
1✔
433

434
        if print_colorized:
1✔
435
            trials2 = trials.astype(str)
1✔
436
            for i in range(len(trials)):
1✔
437
                if trials.loc[i]['is_profit']:
1✔
438
                    for j in range(len(trials.loc[i]) - 3):
1✔
439
                        trials2.iat[i, j] = f"{Fore.GREEN}{str(trials.iloc[i, j])}{Fore.RESET}"
1✔
440
                if trials.loc[i]['is_best'] and highlight_best:
1✔
441
                    for j in range(len(trials.loc[i]) - 3):
1✔
442
                        trials2.iat[i, j] = (
1✔
443
                            f"{Style.BRIGHT}{str(trials.iloc[i, j])}{Style.RESET_ALL}"
444
                        )
445
            trials = trials2
1✔
446
            del trials2
1✔
447
        trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit', 'is_random'])
1✔
448
        if remove_header > 0:
1✔
449
            table = tabulate.tabulate(
×
450
                trials.to_dict(orient='list'), tablefmt='orgtbl',
451
                headers='keys', stralign="right"
452
            )
453

454
            table = table.split("\n", remove_header)[remove_header]
×
455
        elif remove_header < 0:
1✔
456
            table = tabulate.tabulate(
1✔
457
                trials.to_dict(orient='list'), tablefmt='psql',
458
                headers='keys', stralign="right"
459
            )
460
            table = "\n".join(table.split("\n")[0:remove_header])
1✔
461
        else:
462
            table = tabulate.tabulate(
1✔
463
                trials.to_dict(orient='list'), tablefmt='psql',
464
                headers='keys', stralign="right"
465
            )
466
        return table
1✔
467

468
    @staticmethod
1✔
469
    def export_csv_file(config: Config, results: list, csv_file: str) -> None:
1✔
470
        """
471
        Log result to csv-file
472
        """
473
        if not results:
1✔
474
            return
×
475

476
        # Verification for overwrite
477
        if Path(csv_file).is_file():
1✔
478
            logger.error(f"CSV file already exists: {csv_file}")
×
479
            return
×
480

481
        try:
1✔
482
            Path(csv_file).open('w+').close()
1✔
483
        except OSError:
×
484
            logger.error(f"Failed to create CSV file: {csv_file}")
×
485
            return
×
486

487
        trials = json_normalize(results, max_level=1)
1✔
488
        trials['Best'] = ''
1✔
489
        trials['Stake currency'] = config['stake_currency']
1✔
490

491
        base_metrics = ['Best', 'current_epoch', 'results_metrics.total_trades',
1✔
492
                        'results_metrics.profit_mean', 'results_metrics.profit_median',
493
                        'results_metrics.profit_total', 'Stake currency',
494
                        'results_metrics.profit_total_abs', 'results_metrics.holding_avg',
495
                        'results_metrics.trade_count_long', 'results_metrics.trade_count_short',
496
                        'loss', 'is_initial_point', 'is_best']
497
        perc_multi = 100
1✔
498

499
        param_metrics = [("params_dict." + param) for param in results[0]['params_dict'].keys()]
1✔
500
        trials = trials[base_metrics + param_metrics]
1✔
501

502
        base_columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Median profit', 'Total profit',
1✔
503
                        'Stake currency', 'Profit', 'Avg duration',
504
                        'Trade count long', 'Trade count short',
505
                        'Objective',
506
                        'is_initial_point', 'is_best']
507
        param_columns = list(results[0]['params_dict'].keys())
1✔
508
        trials.columns = base_columns + param_columns
1✔
509

510
        trials['is_profit'] = False
1✔
511
        trials.loc[trials['is_initial_point'], 'Best'] = '*'
1✔
512
        trials.loc[trials['is_best'], 'Best'] = 'Best'
1✔
513
        trials.loc[trials['is_initial_point'] & trials['is_best'], 'Best'] = '* Best'
1✔
514
        trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
1✔
515
        trials['Epoch'] = trials['Epoch'].astype(str)
1✔
516
        trials['Trades'] = trials['Trades'].astype(str)
1✔
517
        trials['Median profit'] = trials['Median profit'] * perc_multi
1✔
518

519
        trials['Total profit'] = trials['Total profit'].apply(
1✔
520
            lambda x: f'{x:,.8f}' if x != 0.0 else ""
521
        )
522
        trials['Profit'] = trials['Profit'].apply(
1✔
523
            lambda x: f'{x:,.2f}' if not isna(x) else ""
524
        )
525
        trials['Avg profit'] = trials['Avg profit'].apply(
1✔
526
            lambda x: f'{x * perc_multi:,.2f}%' if not isna(x) else ""
527
        )
528
        trials['Objective'] = trials['Objective'].apply(
1✔
529
            lambda x: f'{x:,.5f}' if x != 100000 else ""
530
        )
531

532
        trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
1✔
533
        trials.to_csv(csv_file, index=False, header=True, mode='w', encoding='UTF-8')
1✔
534
        logger.info(f"CSV file created: {csv_file}")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc