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

freqtrade / freqtrade / 4571845396

pending completion
4571845396

push

github-actions

GitHub
Merge pull request #8386 from freqtrade/feature/price_to_precision_round

40 of 40 new or added lines in 7 files covered. (100.0%)

17492 of 18505 relevant lines covered (94.53%)

0.95 hits per line

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

64.95
/freqtrade/freqai/data_drawer.py
1
import collections
1✔
2
import importlib
1✔
3
import logging
1✔
4
import re
1✔
5
import shutil
1✔
6
import threading
1✔
7
from datetime import datetime, timedelta, timezone
1✔
8
from pathlib import Path
1✔
9
from typing import Any, Dict, Tuple, TypedDict
1✔
10

11
import numpy as np
1✔
12
import pandas as pd
1✔
13
import psutil
1✔
14
import rapidjson
1✔
15
from joblib import dump, load
1✔
16
from joblib.externals import cloudpickle
1✔
17
from numpy.typing import NDArray
1✔
18
from pandas import DataFrame
1✔
19

20
from freqtrade.configuration import TimeRange
1✔
21
from freqtrade.constants import Config
1✔
22
from freqtrade.data.history import load_pair_history
1✔
23
from freqtrade.exceptions import OperationalException
1✔
24
from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
1✔
25
from freqtrade.strategy.interface import IStrategy
1✔
26

27

28
logger = logging.getLogger(__name__)
1✔
29

30

31
class pair_info(TypedDict):
1✔
32
    model_filename: str
1✔
33
    trained_timestamp: int
1✔
34
    data_path: str
1✔
35
    extras: dict
1✔
36

37

38
class FreqaiDataDrawer:
1✔
39
    """
40
    Class aimed at holding all pair models/info in memory for better inferencing/retrainig/saving
41
    /loading to/from disk.
42
    This object remains persistent throughout live/dry.
43

44
    Record of contribution:
45
    FreqAI was developed by a group of individuals who all contributed specific skillsets to the
46
    project.
47

48
    Conception and software development:
49
    Robert Caulk @robcaulk
50

51
    Theoretical brainstorming:
52
    Elin Törnquist @th0rntwig
53

54
    Code review, software architecture brainstorming:
55
    @xmatthias
56

57
    Beta testing and bug reporting:
58
    @bloodhunter4rc, Salah Lamkadem @ikonx, @ken11o2, @longyu, @paranoidandy, @smidelis, @smarm
59
    Juha Nykänen @suikula, Wagner Costa @wagnercosta, Johan Vlugt @Jooopieeert
60
    """
61

62
    def __init__(self, full_path: Path, config: Config):
1✔
63

64
        self.config = config
1✔
65
        self.freqai_info = config.get("freqai", {})
1✔
66
        # dictionary holding all pair metadata necessary to load in from disk
67
        self.pair_dict: Dict[str, pair_info] = {}
1✔
68
        # dictionary holding all actively inferenced models in memory given a model filename
69
        self.model_dictionary: Dict[str, Any] = {}
1✔
70
        # all additional metadata that we want to keep in ram
71
        self.meta_data_dictionary: Dict[str, Dict[str, Any]] = {}
1✔
72
        self.model_return_values: Dict[str, DataFrame] = {}
1✔
73
        self.historic_data: Dict[str, Dict[str, DataFrame]] = {}
1✔
74
        self.historic_predictions: Dict[str, DataFrame] = {}
1✔
75
        self.full_path = full_path
1✔
76
        self.historic_predictions_path = Path(self.full_path / "historic_predictions.pkl")
1✔
77
        self.historic_predictions_bkp_path = Path(
1✔
78
            self.full_path / "historic_predictions.backup.pkl")
79
        self.pair_dictionary_path = Path(self.full_path / "pair_dictionary.json")
1✔
80
        self.global_metadata_path = Path(self.full_path / "global_metadata.json")
1✔
81
        self.metric_tracker_path = Path(self.full_path / "metric_tracker.json")
1✔
82
        self.load_drawer_from_disk()
1✔
83
        self.load_historic_predictions_from_disk()
1✔
84
        self.metric_tracker: Dict[str, Dict[str, Dict[str, list]]] = {}
1✔
85
        self.load_metric_tracker_from_disk()
1✔
86
        self.training_queue: Dict[str, int] = {}
1✔
87
        self.history_lock = threading.Lock()
1✔
88
        self.save_lock = threading.Lock()
1✔
89
        self.pair_dict_lock = threading.Lock()
1✔
90
        self.metric_tracker_lock = threading.Lock()
1✔
91
        self.old_DBSCAN_eps: Dict[str, float] = {}
1✔
92
        self.empty_pair_dict: pair_info = {
1✔
93
                "model_filename": "", "trained_timestamp": 0,
94
                "data_path": "", "extras": {}}
95
        self.model_type = self.freqai_info.get('model_save_type', 'joblib')
1✔
96

97
    def update_metric_tracker(self, metric: str, value: float, pair: str) -> None:
1✔
98
        """
99
        General utility for adding and updating custom metrics. Typically used
100
        for adding training performance, train timings, inferenc timings, cpu loads etc.
101
        """
102
        with self.metric_tracker_lock:
×
103
            if pair not in self.metric_tracker:
×
104
                self.metric_tracker[pair] = {}
×
105
            if metric not in self.metric_tracker[pair]:
×
106
                self.metric_tracker[pair][metric] = {'timestamp': [], 'value': []}
×
107

108
            timestamp = int(datetime.now(timezone.utc).timestamp())
×
109
            self.metric_tracker[pair][metric]['value'].append(value)
×
110
            self.metric_tracker[pair][metric]['timestamp'].append(timestamp)
×
111

112
    def collect_metrics(self, time_spent: float, pair: str):
1✔
113
        """
114
        Add metrics to the metric tracker dictionary
115
        """
116
        load1, load5, load15 = psutil.getloadavg()
×
117
        cpus = psutil.cpu_count()
×
118
        self.update_metric_tracker('train_time', time_spent, pair)
×
119
        self.update_metric_tracker('cpu_load1min', load1 / cpus, pair)
×
120
        self.update_metric_tracker('cpu_load5min', load5 / cpus, pair)
×
121
        self.update_metric_tracker('cpu_load15min', load15 / cpus, pair)
×
122

123
    def load_global_metadata_from_disk(self):
1✔
124
        """
125
        Locate and load a previously saved global metadata in present model folder.
126
        """
127
        exists = self.global_metadata_path.is_file()
1✔
128
        if exists:
1✔
129
            with self.global_metadata_path.open("r") as fp:
1✔
130
                metatada_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
1✔
131
                return metatada_dict
1✔
132
        return {}
1✔
133

134
    def load_drawer_from_disk(self):
1✔
135
        """
136
        Locate and load a previously saved data drawer full of all pair model metadata in
137
        present model folder.
138
        Load any existing metric tracker that may be present.
139
        """
140
        exists = self.pair_dictionary_path.is_file()
1✔
141
        if exists:
1✔
142
            with self.pair_dictionary_path.open("r") as fp:
1✔
143
                self.pair_dict = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
1✔
144
        else:
145
            logger.info("Could not find existing datadrawer, starting from scratch")
1✔
146

147
    def load_metric_tracker_from_disk(self):
1✔
148
        """
149
        Tries to load an existing metrics dictionary if the user
150
        wants to collect metrics.
151
        """
152
        if self.freqai_info.get('write_metrics_to_disk', False):
1✔
153
            exists = self.metric_tracker_path.is_file()
×
154
            if exists:
×
155
                with self.metric_tracker_path.open("r") as fp:
×
156
                    self.metric_tracker = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
×
157
                logger.info("Loading existing metric tracker from disk.")
×
158
            else:
159
                logger.info("Could not find existing metric tracker, starting from scratch")
×
160

161
    def load_historic_predictions_from_disk(self):
1✔
162
        """
163
        Locate and load a previously saved historic predictions.
164
        :return: bool - whether or not the drawer was located
165
        """
166
        exists = self.historic_predictions_path.is_file()
1✔
167
        if exists:
1✔
168
            try:
1✔
169
                with self.historic_predictions_path.open("rb") as fp:
1✔
170
                    self.historic_predictions = cloudpickle.load(fp)
1✔
171
                logger.info(
1✔
172
                    f"Found existing historic predictions at {self.full_path}, but beware "
173
                    "that statistics may be inaccurate if the bot has been offline for "
174
                    "an extended period of time."
175
                )
176
            except EOFError:
×
177
                logger.warning(
×
178
                    'Historical prediction file was corrupted. Trying to load backup file.')
179
                with self.historic_predictions_bkp_path.open("rb") as fp:
×
180
                    self.historic_predictions = cloudpickle.load(fp)
×
181
                logger.warning('FreqAI successfully loaded the backup historical predictions file.')
×
182

183
        else:
184
            logger.info("Could not find existing historic_predictions, starting from scratch")
1✔
185

186
        return exists
1✔
187

188
    def save_historic_predictions_to_disk(self):
1✔
189
        """
190
        Save historic predictions pickle to disk
191
        """
192
        with self.historic_predictions_path.open("wb") as fp:
1✔
193
            cloudpickle.dump(self.historic_predictions, fp, protocol=cloudpickle.DEFAULT_PROTOCOL)
1✔
194

195
        # create a backup
196
        shutil.copy(self.historic_predictions_path, self.historic_predictions_bkp_path)
1✔
197

198
    def save_metric_tracker_to_disk(self):
1✔
199
        """
200
        Save metric tracker of all pair metrics collected.
201
        """
202
        with self.save_lock:
1✔
203
            with self.metric_tracker_path.open('w') as fp:
1✔
204
                rapidjson.dump(self.metric_tracker, fp, default=self.np_encoder,
1✔
205
                               number_mode=rapidjson.NM_NATIVE)
206

207
    def save_drawer_to_disk(self) -> None:
1✔
208
        """
209
        Save data drawer full of all pair model metadata in present model folder.
210
        """
211
        with self.save_lock:
1✔
212
            with self.pair_dictionary_path.open('w') as fp:
1✔
213
                rapidjson.dump(self.pair_dict, fp, default=self.np_encoder,
1✔
214
                               number_mode=rapidjson.NM_NATIVE)
215

216
    def save_global_metadata_to_disk(self, metadata: Dict[str, Any]):
1✔
217
        """
218
        Save global metadata json to disk
219
        """
220
        with self.save_lock:
1✔
221
            with self.global_metadata_path.open('w') as fp:
1✔
222
                rapidjson.dump(metadata, fp, default=self.np_encoder,
1✔
223
                               number_mode=rapidjson.NM_NATIVE)
224

225
    def np_encoder(self, object):
1✔
226
        if isinstance(object, np.generic):
1✔
227
            return object.item()
1✔
228

229
    def get_pair_dict_info(self, pair: str) -> Tuple[str, int]:
1✔
230
        """
231
        Locate and load existing model metadata from persistent storage. If not located,
232
        create a new one and append the current pair to it and prepare it for its first
233
        training
234
        :param pair: str: pair to lookup
235
        :return:
236
            model_filename: str = unique filename used for loading persistent objects from disk
237
            trained_timestamp: int = the last time the coin was trained
238
        """
239

240
        pair_dict = self.pair_dict.get(pair)
1✔
241

242
        if pair_dict:
1✔
243
            model_filename = pair_dict["model_filename"]
1✔
244
            trained_timestamp = pair_dict["trained_timestamp"]
1✔
245
        else:
246
            self.pair_dict[pair] = self.empty_pair_dict.copy()
1✔
247
            model_filename = ""
1✔
248
            trained_timestamp = 0
1✔
249

250
        return model_filename, trained_timestamp
1✔
251

252
    def set_pair_dict_info(self, metadata: dict) -> None:
1✔
253
        pair_in_dict = self.pair_dict.get(metadata["pair"])
×
254
        if pair_in_dict:
×
255
            return
×
256
        else:
257
            self.pair_dict[metadata["pair"]] = self.empty_pair_dict.copy()
×
258
            return
×
259

260
    def set_initial_return_values(self, pair: str, pred_df: DataFrame) -> None:
1✔
261
        """
262
        Set the initial return values to the historical predictions dataframe. This avoids needing
263
        to repredict on historical candles, and also stores historical predictions despite
264
        retrainings (so stored predictions are true predictions, not just inferencing on trained
265
        data)
266
        """
267

268
        hist_df = self.historic_predictions
×
269
        len_diff = len(hist_df[pair].index) - len(pred_df.index)
×
270
        if len_diff < 0:
×
271
            df_concat = pd.concat([pred_df.iloc[:abs(len_diff)], hist_df[pair]],
×
272
                                  ignore_index=True, keys=hist_df[pair].keys())
273
        else:
274
            df_concat = hist_df[pair].tail(len(pred_df.index)).reset_index(drop=True)
×
275
        df_concat = df_concat.fillna(0)
×
276
        self.model_return_values[pair] = df_concat
×
277

278
    def append_model_predictions(self, pair: str, predictions: DataFrame,
1✔
279
                                 do_preds: NDArray[np.int_],
280
                                 dk: FreqaiDataKitchen, strat_df: DataFrame) -> None:
281
        """
282
        Append model predictions to historic predictions dataframe, then set the
283
        strategy return dataframe to the tail of the historic predictions. The length of
284
        the tail is equivalent to the length of the dataframe that entered FreqAI from
285
        the strategy originally. Doing this allows FreqUI to always display the correct
286
        historic predictions.
287
        """
288

289
        len_df = len(strat_df)
×
290
        index = self.historic_predictions[pair].index[-1:]
×
291
        columns = self.historic_predictions[pair].columns
×
292

293
        nan_df = pd.DataFrame(np.nan, index=index, columns=columns)
×
294
        self.historic_predictions[pair] = pd.concat(
×
295
            [self.historic_predictions[pair], nan_df], ignore_index=True, axis=0)
296
        df = self.historic_predictions[pair]
×
297

298
        # model outputs and associated statistics
299
        for label in predictions.columns:
×
300
            df[label].iloc[-1] = predictions[label].iloc[-1]
×
301
            if df[label].dtype == object:
×
302
                continue
×
303
            df[f"{label}_mean"].iloc[-1] = dk.data["labels_mean"][label]
×
304
            df[f"{label}_std"].iloc[-1] = dk.data["labels_std"][label]
×
305

306
        # outlier indicators
307
        df["do_predict"].iloc[-1] = do_preds[-1]
×
308
        if self.freqai_info["feature_parameters"].get("DI_threshold", 0) > 0:
×
309
            df["DI_values"].iloc[-1] = dk.DI_values[-1]
×
310

311
        # extra values the user added within custom prediction model
312
        if dk.data['extra_returns_per_train']:
×
313
            rets = dk.data['extra_returns_per_train']
×
314
            for return_str in rets:
×
315
                df[return_str].iloc[-1] = rets[return_str]
×
316

317
        # this logic carries users between version without needing to
318
        # change their identifier
319
        if 'close_price' not in df.columns:
×
320
            df['close_price'] = np.nan
×
321
            df['date_pred'] = np.nan
×
322

323
        df['close_price'].iloc[-1] = strat_df['close'].iloc[-1]
×
324
        df['date_pred'].iloc[-1] = strat_df['date'].iloc[-1]
×
325

326
        self.model_return_values[pair] = df.tail(len_df).reset_index(drop=True)
×
327

328
    def attach_return_values_to_return_dataframe(
1✔
329
            self, pair: str, dataframe: DataFrame) -> DataFrame:
330
        """
331
        Attach the return values to the strat dataframe
332
        :param dataframe: DataFrame = strategy dataframe
333
        :return: DataFrame = strat dataframe with return values attached
334
        """
335
        df = self.model_return_values[pair]
×
336
        to_keep = [col for col in dataframe.columns if not col.startswith("&")]
×
337
        dataframe = pd.concat([dataframe[to_keep], df], axis=1)
×
338
        return dataframe
×
339

340
    def return_null_values_to_strategy(self, dataframe: DataFrame, dk: FreqaiDataKitchen) -> None:
1✔
341
        """
342
        Build 0 filled dataframe to return to strategy
343
        """
344

345
        dk.find_features(dataframe)
×
346
        dk.find_labels(dataframe)
×
347

348
        full_labels = dk.label_list + dk.unique_class_list
×
349

350
        for label in full_labels:
×
351
            dataframe[label] = 0
×
352
            dataframe[f"{label}_mean"] = 0
×
353
            dataframe[f"{label}_std"] = 0
×
354

355
        dataframe["do_predict"] = 0
×
356

357
        if self.freqai_info["feature_parameters"].get("DI_threshold", 0) > 0:
×
358
            dataframe["DI_values"] = 0
×
359

360
        if dk.data['extra_returns_per_train']:
×
361
            rets = dk.data['extra_returns_per_train']
×
362
            for return_str in rets:
×
363
                dataframe[return_str] = 0
×
364

365
        dk.return_dataframe = dataframe
×
366

367
    def purge_old_models(self) -> None:
1✔
368

369
        num_keep = self.freqai_info["purge_old_models"]
1✔
370
        if not num_keep:
1✔
371
            return
×
372
        elif type(num_keep) == bool:
1✔
373
            num_keep = 2
×
374

375
        model_folders = [x for x in self.full_path.iterdir() if x.is_dir()]
1✔
376

377
        pattern = re.compile(r"sub-train-(\w+)_(\d{10})")
1✔
378

379
        delete_dict: Dict[str, Any] = {}
1✔
380

381
        for dir in model_folders:
1✔
382
            result = pattern.match(str(dir.name))
1✔
383
            if result is None:
1✔
384
                continue
1✔
385
            coin = result.group(1)
1✔
386
            timestamp = result.group(2)
1✔
387

388
            if coin not in delete_dict:
1✔
389
                delete_dict[coin] = {}
1✔
390
                delete_dict[coin]["num_folders"] = 1
1✔
391
                delete_dict[coin]["timestamps"] = {int(timestamp): dir}
1✔
392
            else:
393
                delete_dict[coin]["num_folders"] += 1
×
394
                delete_dict[coin]["timestamps"][int(timestamp)] = dir
×
395

396
        for coin in delete_dict:
1✔
397
            if delete_dict[coin]["num_folders"] > num_keep:
1✔
398
                sorted_dict = collections.OrderedDict(
×
399
                    sorted(delete_dict[coin]["timestamps"].items())
400
                )
401
                num_delete = len(sorted_dict) - num_keep
×
402
                deleted = 0
×
403
                for k, v in sorted_dict.items():
×
404
                    if deleted >= num_delete:
×
405
                        break
×
406
                    logger.info(f"Freqai purging old model file {v}")
×
407
                    shutil.rmtree(v)
×
408
                    deleted += 1
×
409

410
    def save_metadata(self, dk: FreqaiDataKitchen) -> None:
1✔
411
        """
412
        Saves only metadata for backtesting studies if user prefers
413
        not to save model data. This saves tremendous amounts of space
414
        for users generating huge studies.
415
        This is only active when `save_backtest_models`: false (not default)
416
        """
417
        if not dk.data_path.is_dir():
×
418
            dk.data_path.mkdir(parents=True, exist_ok=True)
×
419

420
        save_path = Path(dk.data_path)
×
421

422
        dk.data["data_path"] = str(dk.data_path)
×
423
        dk.data["model_filename"] = str(dk.model_filename)
×
424
        dk.data["training_features_list"] = list(dk.data_dictionary["train_features"].columns)
×
425
        dk.data["label_list"] = dk.label_list
×
426

427
        with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp:
×
428
            rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
×
429

430
        return
×
431

432
    def save_data(self, model: Any, coin: str, dk: FreqaiDataKitchen) -> None:
1✔
433
        """
434
        Saves all data associated with a model for a single sub-train time range
435
        :param model: User trained model which can be reused for inferencing to generate
436
                      predictions
437
        """
438

439
        if not dk.data_path.is_dir():
1✔
440
            dk.data_path.mkdir(parents=True, exist_ok=True)
1✔
441

442
        save_path = Path(dk.data_path)
1✔
443

444
        # Save the trained model
445
        if self.model_type == 'joblib':
1✔
446
            dump(model, save_path / f"{dk.model_filename}_model.joblib")
1✔
447
        elif self.model_type == 'keras':
1✔
448
            model.save(save_path / f"{dk.model_filename}_model.h5")
×
449
        elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type:
1✔
450
            model.save(save_path / f"{dk.model_filename}_model.zip")
1✔
451

452
        if dk.svm_model is not None:
1✔
453
            dump(dk.svm_model, save_path / f"{dk.model_filename}_svm_model.joblib")
1✔
454

455
        dk.data["data_path"] = str(dk.data_path)
1✔
456
        dk.data["model_filename"] = str(dk.model_filename)
1✔
457
        dk.data["training_features_list"] = dk.training_features_list
1✔
458
        dk.data["label_list"] = dk.label_list
1✔
459
        # store the metadata
460
        with (save_path / f"{dk.model_filename}_metadata.json").open("w") as fp:
1✔
461
            rapidjson.dump(dk.data, fp, default=self.np_encoder, number_mode=rapidjson.NM_NATIVE)
1✔
462

463
        # save the train data to file so we can check preds for area of applicability later
464
        dk.data_dictionary["train_features"].to_pickle(
1✔
465
            save_path / f"{dk.model_filename}_trained_df.pkl"
466
        )
467

468
        dk.data_dictionary["train_dates"].to_pickle(
1✔
469
            save_path / f"{dk.model_filename}_trained_dates_df.pkl"
470
        )
471

472
        if self.freqai_info["feature_parameters"].get("principal_component_analysis"):
1✔
473
            cloudpickle.dump(
1✔
474
                dk.pca, (dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("wb")
475
            )
476

477
        self.model_dictionary[coin] = model
1✔
478
        self.pair_dict[coin]["model_filename"] = dk.model_filename
1✔
479
        self.pair_dict[coin]["data_path"] = str(dk.data_path)
1✔
480

481
        if coin not in self.meta_data_dictionary:
1✔
482
            self.meta_data_dictionary[coin] = {}
1✔
483
        self.meta_data_dictionary[coin]["train_df"] = dk.data_dictionary["train_features"]
1✔
484
        self.meta_data_dictionary[coin]["meta_data"] = dk.data
1✔
485
        self.save_drawer_to_disk()
1✔
486

487
        return
1✔
488

489
    def load_metadata(self, dk: FreqaiDataKitchen) -> None:
1✔
490
        """
491
        Load only metadata into datakitchen to increase performance during
492
        presaved backtesting (prediction file loading).
493
        """
494
        with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp:
1✔
495
            dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
1✔
496
            dk.training_features_list = dk.data["training_features_list"]
1✔
497
            dk.label_list = dk.data["label_list"]
1✔
498

499
    def load_data(self, coin: str, dk: FreqaiDataKitchen) -> Any:
1✔
500
        """
501
        loads all data required to make a prediction on a sub-train time range
502
        :returns:
503
        :model: User trained model which can be inferenced for new predictions
504
        """
505

506
        if not self.pair_dict[coin]["model_filename"]:
1✔
507
            return None
×
508

509
        if dk.live:
1✔
510
            dk.model_filename = self.pair_dict[coin]["model_filename"]
×
511
            dk.data_path = Path(self.pair_dict[coin]["data_path"])
×
512

513
        if coin in self.meta_data_dictionary:
1✔
514
            dk.data = self.meta_data_dictionary[coin]["meta_data"]
1✔
515
            dk.data_dictionary["train_features"] = self.meta_data_dictionary[coin]["train_df"]
1✔
516
        else:
517
            with (dk.data_path / f"{dk.model_filename}_metadata.json").open("r") as fp:
×
518
                dk.data = rapidjson.load(fp, number_mode=rapidjson.NM_NATIVE)
×
519

520
            dk.data_dictionary["train_features"] = pd.read_pickle(
×
521
                dk.data_path / f"{dk.model_filename}_trained_df.pkl"
522
            )
523

524
        dk.training_features_list = dk.data["training_features_list"]
1✔
525
        dk.label_list = dk.data["label_list"]
1✔
526

527
        # try to access model in memory instead of loading object from disk to save time
528
        if dk.live and coin in self.model_dictionary:
1✔
529
            model = self.model_dictionary[coin]
×
530
        elif self.model_type == 'joblib':
1✔
531
            model = load(dk.data_path / f"{dk.model_filename}_model.joblib")
1✔
532
        elif self.model_type == 'keras':
×
533
            from tensorflow import keras
×
534
            model = keras.models.load_model(dk.data_path / f"{dk.model_filename}_model.h5")
×
535
        elif 'stable_baselines' in self.model_type or 'sb3_contrib' == self.model_type:
×
536
            mod = importlib.import_module(
×
537
                self.model_type, self.freqai_info['rl_config']['model_type'])
538
            MODELCLASS = getattr(mod, self.freqai_info['rl_config']['model_type'])
×
539
            model = MODELCLASS.load(dk.data_path / f"{dk.model_filename}_model")
×
540

541
        if Path(dk.data_path / f"{dk.model_filename}_svm_model.joblib").is_file():
1✔
542
            dk.svm_model = load(dk.data_path / f"{dk.model_filename}_svm_model.joblib")
1✔
543

544
        if not model:
1✔
545
            raise OperationalException(
×
546
                f"Unable to load model, ensure model exists at " f"{dk.data_path} "
547
            )
548

549
        # load it into ram if it was loaded from disk
550
        if coin not in self.model_dictionary:
1✔
551
            self.model_dictionary[coin] = model
×
552

553
        if self.config["freqai"]["feature_parameters"]["principal_component_analysis"]:
1✔
554
            dk.pca = cloudpickle.load(
×
555
                (dk.data_path / f"{dk.model_filename}_pca_object.pkl").open("rb")
556
            )
557

558
        return model
1✔
559

560
    def update_historic_data(self, strategy: IStrategy, dk: FreqaiDataKitchen) -> None:
1✔
561
        """
562
        Append new candles to our stores historic data (in memory) so that
563
        we do not need to load candle history from disk and we dont need to
564
        pinging exchange multiple times for the same candle.
565
        :param dataframe: DataFrame = strategy provided dataframe
566
        """
567
        feat_params = self.freqai_info["feature_parameters"]
1✔
568
        with self.history_lock:
1✔
569
            history_data = self.historic_data
1✔
570

571
            for pair in dk.all_pairs:
1✔
572
                for tf in feat_params.get("include_timeframes"):
1✔
573
                    hist_df = history_data[pair][tf]
1✔
574
                    # check if newest candle is already appended
575
                    df_dp = strategy.dp.get_pair_dataframe(pair, tf)
1✔
576
                    if len(df_dp.index) == 0:
1✔
577
                        continue
×
578
                    if str(hist_df.iloc[-1]["date"]) == str(
1✔
579
                        df_dp.iloc[-1:]["date"].iloc[-1]
580
                    ):
581
                        continue
×
582

583
                    try:
1✔
584
                        index = (
1✔
585
                            df_dp.loc[
586
                                df_dp["date"] == hist_df.iloc[-1]["date"]
587
                            ].index[0]
588
                            + 1
589
                        )
590
                    except IndexError:
×
591
                        if hist_df.iloc[-1]['date'] < df_dp['date'].iloc[0]:
×
592
                            raise OperationalException("In memory historical data is older than "
×
593
                                                       f"oldest DataProvider candle for {pair} on "
594
                                                       f"timeframe {tf}")
595
                        else:
596
                            index = -1
×
597
                            logger.warning(
×
598
                                f"No common dates in historical data and dataprovider for {pair}. "
599
                                f"Appending latest dataprovider candle to historical data "
600
                                "but please be aware that there is likely a gap in the historical "
601
                                "data. \n"
602
                                f"Historical data ends at {hist_df.iloc[-1]['date']} "
603
                                f"while dataprovider starts at {df_dp['date'].iloc[0]} and"
604
                                f"ends at {df_dp['date'].iloc[0]}."
605
                            )
606

607
                    history_data[pair][tf] = pd.concat(
1✔
608
                        [
609
                            hist_df,
610
                            df_dp.iloc[index:],
611
                        ],
612
                        ignore_index=True,
613
                        axis=0,
614
                    )
615

616
            self.current_candle = history_data[dk.pair][self.config['timeframe']].iloc[-1]['date']
1✔
617

618
    def load_all_pair_histories(self, timerange: TimeRange, dk: FreqaiDataKitchen) -> None:
1✔
619
        """
620
        Load pair histories for all whitelist and corr_pairlist pairs.
621
        Only called once upon startup of bot.
622
        :param timerange: TimeRange = full timerange required to populate all indicators
623
                          for training according to user defined train_period_days
624
        """
625
        history_data = self.historic_data
1✔
626

627
        for pair in dk.all_pairs:
1✔
628
            if pair not in history_data:
1✔
629
                history_data[pair] = {}
1✔
630
            for tf in self.freqai_info["feature_parameters"].get("include_timeframes"):
1✔
631
                history_data[pair][tf] = load_pair_history(
1✔
632
                    datadir=self.config["datadir"],
633
                    timeframe=tf,
634
                    pair=pair,
635
                    timerange=timerange,
636
                    data_format=self.config.get("dataformat_ohlcv", "json"),
637
                    candle_type=self.config.get("trading_mode", "spot"),
638
                )
639

640
    def get_base_and_corr_dataframes(
1✔
641
        self, timerange: TimeRange, pair: str, dk: FreqaiDataKitchen
642
    ) -> Tuple[Dict[Any, Any], Dict[Any, Any]]:
643
        """
644
        Searches through our historic_data in memory and returns the dataframes relevant
645
        to the present pair.
646
        :param timerange: TimeRange = full timerange required to populate all indicators
647
                          for training according to user defined train_period_days
648
        :param metadata: dict = strategy furnished pair metadata
649
        """
650
        with self.history_lock:
1✔
651
            corr_dataframes: Dict[Any, Any] = {}
1✔
652
            base_dataframes: Dict[Any, Any] = {}
1✔
653
            historic_data = self.historic_data
1✔
654
            pairs = self.freqai_info["feature_parameters"].get(
1✔
655
                "include_corr_pairlist", []
656
            )
657

658
            for tf in self.freqai_info["feature_parameters"].get("include_timeframes"):
1✔
659
                base_dataframes[tf] = dk.slice_dataframe(
1✔
660
                    timerange, historic_data[pair][tf]).reset_index(drop=True)
661
                if pairs:
1✔
662
                    for p in pairs:
1✔
663
                        if pair in p:
1✔
664
                            continue  # dont repeat anything from whitelist
1✔
665
                        if p not in corr_dataframes:
1✔
666
                            corr_dataframes[p] = {}
1✔
667
                        corr_dataframes[p][tf] = dk.slice_dataframe(
1✔
668
                            timerange, historic_data[p][tf]
669
                        ).reset_index(drop=True)
670

671
        return corr_dataframes, base_dataframes
1✔
672

673
    def get_timerange_from_live_historic_predictions(self) -> TimeRange:
1✔
674
        """
675
        Returns timerange information based on historic predictions file
676
        :return: timerange calculated from saved live data
677
        """
678
        if not self.historic_predictions_path.is_file():
1✔
679
            raise OperationalException(
1✔
680
                'Historic predictions not found. Historic predictions data is required '
681
                'to run backtest with the freqai-backtest-live-models option '
682
            )
683

684
        self.load_historic_predictions_from_disk()
1✔
685

686
        all_pairs_end_dates = []
1✔
687
        for pair in self.historic_predictions:
1✔
688
            pair_historic_data = self.historic_predictions[pair]
1✔
689
            all_pairs_end_dates.append(pair_historic_data.date_pred.max())
1✔
690

691
        global_metadata = self.load_global_metadata_from_disk()
1✔
692
        start_date = datetime.fromtimestamp(int(global_metadata["start_dry_live_date"]))
1✔
693
        end_date = max(all_pairs_end_dates)
1✔
694
        # add 1 day to string timerange to ensure BT module will load all dataframe data
695
        end_date = end_date + timedelta(days=1)
1✔
696
        backtesting_timerange = TimeRange(
1✔
697
            'date', 'date', int(start_date.timestamp()), int(end_date.timestamp())
698
        )
699
        return backtesting_timerange
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