Skip to content

Commit c01f25d

Browse files
committed
update code to freqai_backtest_live_models only from historic predictions
1 parent fdc82af commit c01f25d

9 files changed

+38
-229
lines changed

docs/freqai-parameter-table.md

-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ Mandatory parameters are marked as **Required** and have to be set in one of the
1515
| `expiration_hours` | Avoid making predictions if a model is more than `expiration_hours` old. <br> **Datatype:** Positive integer. <br> Default: `0` (models never expire).
1616
| `purge_old_models` | Delete obsolete models. <br> **Datatype:** Boolean. <br> Default: `False` (all historic models remain on disk).
1717
| `save_backtest_models` | Save models to disk when running backtesting. Backtesting operates most efficiently by saving the prediction data and reusing them directly for subsequent runs (when you wish to tune entry/exit parameters). Saving backtesting models to disk also allows to use the same model files for starting a dry/live instance with the same model `identifier`. <br> **Datatype:** Boolean. <br> Default: `False` (no models are saved).
18-
| `backtest_using_historic_predictions` | Reuse `historic_predictions` in backtesting with [Backtest live models](freqai-running.md#backtest_live_models)) option. <br> Default: `True`
1918
| `fit_live_predictions_candles` | Number of historical candles to use for computing target (label) statistics from prediction data, instead of from the training dataset (more information can be found [here](freqai-configuration.md#creating-a-dynamic-target-threshold)). <br> **Datatype:** Positive integer.
2019
| `follow_mode` | Use a `follower` that will look for models associated with a specific `identifier` and load those for inferencing. A `follower` will **not** train new models. <br> **Datatype:** Boolean. <br> Default: `False`.
2120
| `continual_learning` | Use the final state of the most recently trained model as starting point for the new model, allowing for incremental learning (more information can be found [here](freqai-running.md#continual-learning)). <br> **Datatype:** Boolean. <br> Default: `False`.

docs/freqai-running.md

+2-10
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,9 @@ To save the models generated during a particular backtest so that you can start
8181

8282
### Backtest live models
8383

84-
FreqAI allow you to reuse ready models through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse predictions generated in dry/run for comparison or other study. For that, you have 2 options:
84+
FreqAI allow you to reuse live historic predictions through the backtest parameter `--freqai-backtest-live-models`. This can be useful when you want to reuse predictions generated in dry/run for comparison or other study.
8585

86-
1. Set `"backtest_using_historic_predictions"` to `True` in the config. With this option, FreqAI will reuse `historic_predictions` in backtesting. This option requires less disk space and backtesting will run faster.
87-
2. Set `"purge_old_models"` to `False` and `"backtest_using_historic_predictions"` to `False` in the config. In this case, FreqAI will use the saved models to make the predictions in backtesting. This option requires more disk space and the backtest will have a longer execution time.
88-
89-
The `--timerange` parameter must not be informed, as it will be automatically calculated through the training end dates of the models.
90-
91-
Each model has an identifier derived from the training end date. If you have only 1 model trained, FreqAI will backtest from the training end date until the current date. If you have more than 1 model, each model will perform the backtesting according to the training end date until the training end date of the next model and so on. For the last model, the period of the previous model will be used for the execution.
92-
93-
!!! Note
94-
Currently, there is no checking for expired models, even if the `expired_hours` parameter is set.
86+
The `--timerange` parameter must not be informed, as it will be automatically calculated through the data in historic predictions file.
9587

9688

9789
### Downloading data to cover the full backtest period

freqtrade/freqai/data_drawer.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ def get_base_and_corr_dataframes(
715715

716716
return corr_dataframes, base_dataframes
717717

718-
def get_timerange_from_backtesting_live_dataframe(self) -> TimeRange:
718+
def get_timerange_from_live_historic_predictions(self) -> TimeRange:
719719
"""
720720
Returns timerange information based on historic predictions file
721721
:return: timerange calculated from saved live data
@@ -724,7 +724,6 @@ def get_timerange_from_backtesting_live_dataframe(self) -> TimeRange:
724724
raise OperationalException(
725725
'Historic predictions not found. Historic predictions data is required '
726726
'to run backtest with the freqai-backtest-live-models option '
727-
'and backtest_using_historic_predictions config option as true'
728727
)
729728

730729
self.load_historic_predictions_from_disk()

freqtrade/freqai/data_kitchen.py

+24-124
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import copy
22
import logging
33
import shutil
4-
from datetime import datetime, timedelta, timezone
4+
from datetime import datetime, timezone
55
from math import cos, sin
66
from pathlib import Path
77
from typing import Any, Dict, List, Tuple
@@ -86,14 +86,7 @@ def __init__(
8686
if not self.live:
8787
self.full_path = self.get_full_models_path(self.config)
8888

89-
if self.backtest_live_models:
90-
if self.pair and not (
91-
self.freqai_config.get("backtest_using_historic_predictions", True)
92-
):
93-
self.set_timerange_from_ready_models()
94-
(self.training_timeranges,
95-
self.backtesting_timeranges) = self.split_timerange_live_models()
96-
else:
89+
if not self.backtest_live_models:
9790
self.full_timerange = self.create_fulltimerange(
9891
self.config["timerange"], self.freqai_config.get("train_period_days", 0)
9992
)
@@ -458,28 +451,28 @@ def split_timerange(
458451
# print(tr_training_list, tr_backtesting_list)
459452
return tr_training_list_timerange, tr_backtesting_list_timerange
460453

461-
def split_timerange_live_models(
462-
self
463-
) -> Tuple[list, list]:
464-
465-
tr_backtesting_list_timerange = []
466-
asset = self.pair.split("/")[0]
467-
if asset not in self.backtest_live_models_data["assets_end_dates"]:
468-
raise OperationalException(
469-
f"Model not available for pair {self.pair}. "
470-
"Please, try again after removing this pair from the configuration file."
471-
)
472-
asset_data = self.backtest_live_models_data["assets_end_dates"][asset]
473-
backtesting_timerange = self.backtest_live_models_data["backtesting_timerange"]
474-
model_end_dates = [x for x in asset_data]
475-
model_end_dates.append(backtesting_timerange.stopts)
476-
model_end_dates.sort()
477-
for index, item in enumerate(model_end_dates):
478-
if len(model_end_dates) > (index + 1):
479-
tr_to_add = TimeRange("date", "date", item, model_end_dates[index + 1])
480-
tr_backtesting_list_timerange.append(tr_to_add)
481-
482-
return tr_backtesting_list_timerange, tr_backtesting_list_timerange
454+
# def split_timerange_live_models(
455+
# self
456+
# ) -> Tuple[list, list]:
457+
458+
# tr_backtesting_list_timerange = []
459+
# asset = self.pair.split("/")[0]
460+
# if asset not in self.backtest_live_models_data["assets_end_dates"]:
461+
# raise OperationalException(
462+
# f"Model not available for pair {self.pair}. "
463+
# "Please, try again after removing this pair from the configuration file."
464+
# )
465+
# asset_data = self.backtest_live_models_data["assets_end_dates"][asset]
466+
# backtesting_timerange = self.backtest_live_models_data["backtesting_timerange"]
467+
# model_end_dates = [x for x in asset_data]
468+
# model_end_dates.append(backtesting_timerange.stopts)
469+
# model_end_dates.sort()
470+
# for index, item in enumerate(model_end_dates):
471+
# if len(model_end_dates) > (index + 1):
472+
# tr_to_add = TimeRange("date", "date", item, model_end_dates[index + 1])
473+
# tr_backtesting_list_timerange.append(tr_to_add)
474+
475+
# return tr_backtesting_list_timerange, tr_backtesting_list_timerange
483476

484477
def slice_dataframe(self, timerange: TimeRange, df: DataFrame) -> DataFrame:
485478
"""
@@ -1371,17 +1364,6 @@ def check_if_backtest_prediction_is_valid(
13711364
)
13721365
return False
13731366

1374-
def set_timerange_from_ready_models(self):
1375-
backtesting_timerange, \
1376-
assets_end_dates = (
1377-
self.get_timerange_and_assets_end_dates_from_ready_models(self.full_path))
1378-
1379-
self.backtest_live_models_data = {
1380-
"backtesting_timerange": backtesting_timerange,
1381-
"assets_end_dates": assets_end_dates
1382-
}
1383-
return
1384-
13851367
def get_full_models_path(self, config: Config) -> Path:
13861368
"""
13871369
Returns default FreqAI model path
@@ -1392,88 +1374,6 @@ def get_full_models_path(self, config: Config) -> Path:
13921374
config["user_data_dir"] / "models" / str(freqai_config.get("identifier"))
13931375
)
13941376

1395-
def get_timerange_and_assets_end_dates_from_ready_models(
1396-
self, models_path: Path) -> Tuple[TimeRange, Dict[str, Any]]:
1397-
"""
1398-
Returns timerange information based on a FreqAI model directory
1399-
:param models_path: FreqAI model path
1400-
1401-
:return: a Tuple with (Timerange calculated from directory and
1402-
a Dict with pair and model end training dates info)
1403-
"""
1404-
all_models_end_dates = []
1405-
assets_end_dates: Dict[str, Any] = self.get_assets_timestamps_training_from_ready_models(
1406-
models_path)
1407-
for key in assets_end_dates:
1408-
for model_end_date in assets_end_dates[key]:
1409-
if model_end_date not in all_models_end_dates:
1410-
all_models_end_dates.append(model_end_date)
1411-
1412-
if len(all_models_end_dates) == 0:
1413-
raise OperationalException(
1414-
'At least 1 saved model is required to '
1415-
'run backtest with the freqai-backtest-live-models option'
1416-
)
1417-
1418-
if len(all_models_end_dates) == 1:
1419-
logger.warning(
1420-
"Only 1 model was found. Backtesting will run with the "
1421-
"timerange from the end of the training date to the current date"
1422-
)
1423-
1424-
finish_timestamp = int(datetime.now(tz=timezone.utc).timestamp())
1425-
if len(all_models_end_dates) > 1:
1426-
# After last model end date, use the same period from previous model
1427-
# to finish the backtest
1428-
all_models_end_dates.sort(reverse=True)
1429-
finish_timestamp = all_models_end_dates[0] + \
1430-
(all_models_end_dates[0] - all_models_end_dates[1])
1431-
1432-
all_models_end_dates.append(finish_timestamp)
1433-
all_models_end_dates.sort()
1434-
start_date = (datetime(*datetime.fromtimestamp(min(all_models_end_dates),
1435-
timezone.utc).timetuple()[:3], tzinfo=timezone.utc))
1436-
end_date = (datetime(*datetime.fromtimestamp(max(all_models_end_dates),
1437-
timezone.utc).timetuple()[:3], tzinfo=timezone.utc))
1438-
1439-
# add 1 day to string timerange to ensure BT module will load all dataframe data
1440-
end_date = end_date + timedelta(days=1)
1441-
backtesting_timerange = TimeRange(
1442-
'date', 'date', int(start_date.timestamp()), int(end_date.timestamp())
1443-
)
1444-
return backtesting_timerange, assets_end_dates
1445-
1446-
def get_assets_timestamps_training_from_ready_models(
1447-
self, models_path: Path) -> Dict[str, Any]:
1448-
"""
1449-
Scan the models path and returns all assets end training dates (timestamp)
1450-
:param models_path: FreqAI model path
1451-
1452-
:return: a Dict with asset and model end training dates info
1453-
"""
1454-
assets_end_dates: Dict[str, Any] = {}
1455-
if not models_path.is_dir():
1456-
raise OperationalException(
1457-
'Model folders not found. Saved models are required '
1458-
'to run backtest with the freqai-backtest-live-models option'
1459-
)
1460-
for model_dir in models_path.iterdir():
1461-
if str(model_dir.name).startswith("sub-train"):
1462-
model_end_date = int(model_dir.name.split("_")[1])
1463-
asset = model_dir.name.split("_")[0].replace("sub-train-", "")
1464-
model_file_name = (
1465-
f"cb_{str(model_dir.name).replace('sub-train-', '').lower()}"
1466-
"_model.joblib"
1467-
)
1468-
1469-
model_path_file = Path(model_dir / model_file_name)
1470-
if model_path_file.is_file():
1471-
if asset not in assets_end_dates:
1472-
assets_end_dates[asset] = []
1473-
assets_end_dates[asset].append(model_end_date)
1474-
1475-
return assets_end_dates
1476-
14771377
def remove_special_chars_from_feature_names(self, dataframe: pd.DataFrame) -> pd.DataFrame:
14781378
"""
14791379
Remove all special characters from feature strings (:)

freqtrade/freqai/freqai_interface.py

+5-14
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,6 @@ def __init__(self, config: Config) -> None:
6868
self.save_backtest_models: bool = self.freqai_info.get("save_backtest_models", True)
6969
if self.save_backtest_models:
7070
logger.info('Backtesting module configured to save all models.')
71-
self.backtest_using_historic_predictions: bool = self.freqai_info.get(
72-
"backtest_using_historic_predictions", True)
73-
if self.backtest_using_historic_predictions:
74-
logger.info('Backtesting live models configured to use historic predictions.')
7571

7672
self.dd = FreqaiDataDrawer(Path(self.full_path), self.config, self.follow_mode)
7773
# set current candle to arbitrary historical date
@@ -148,23 +144,18 @@ def start(self, dataframe: DataFrame, metadata: dict, strategy: IStrategy) -> Da
148144
elif not self.follow_mode:
149145
self.dk = FreqaiDataKitchen(self.config, self.live, metadata["pair"])
150146
if self.dk.backtest_live_models:
151-
if self.backtest_using_historic_predictions:
152-
logger.info(
153-
"Backtesting using historic predictions (live models)")
154-
else:
155-
logger.info(
156-
f"Backtesting {len(self.dk.backtesting_timeranges)} "
157-
"timeranges (live models)")
147+
logger.info(
148+
"Backtesting using historic predictions (live models)")
158149
else:
159150
logger.info(f"Training {len(self.dk.training_timeranges)} timeranges")
160151
dataframe = self.dk.use_strategy_to_populate_indicators(
161152
strategy, prediction_dataframe=dataframe, pair=metadata["pair"]
162153
)
163-
if not self.backtest_using_historic_predictions:
154+
if not self.config.get("freqai_backtest_live_models", False):
164155
dk = self.start_backtesting(dataframe, metadata, self.dk)
165156
dataframe = dk.remove_features_from_df(dk.return_dataframe)
166157
else:
167-
dk = self.start_backtesting_from_live_saved_files(
158+
dk = self.start_backtesting_from_historic_predictions(
168159
dataframe, metadata, self.dk)
169160
dataframe = dk.return_dataframe
170161

@@ -330,7 +321,7 @@ def start_backtesting(
330321

331322
return dk
332323

333-
def start_backtesting_from_live_saved_files(
324+
def start_backtesting_from_historic_predictions(
334325
self, dataframe: DataFrame, metadata: dict, dk: FreqaiDataKitchen
335326
) -> FreqaiDataKitchen:
336327
"""

freqtrade/freqai/utils.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,6 @@ def get_timerange_backtest_live_models(config: Config) -> str:
230230
"""
231231
dk = FreqaiDataKitchen(config)
232232
models_path = dk.get_full_models_path(config)
233-
timerange: TimeRange = TimeRange()
234-
if not config.get("freqai", {}).get("backtest_using_historic_predictions", True):
235-
timerange, _ = dk.get_timerange_and_assets_end_dates_from_ready_models(models_path)
236-
else:
237-
dd = FreqaiDataDrawer(models_path, config)
238-
timerange = dd.get_timerange_from_backtesting_live_dataframe()
239-
233+
dd = FreqaiDataDrawer(models_path, config)
234+
timerange = dd.get_timerange_from_live_historic_predictions()
240235
return timerange.timerange_str

tests/freqai/test_freqai_backtesting.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def test_freqai_backtest_live_models_model_not_found(freqai_conf, mocker, testda
8181
bt_config = setup_optimize_configuration(args, RunMode.BACKTEST)
8282

8383
with pytest.raises(OperationalException,
84-
match=r".* Saved models are required to run backtest .*"):
84+
match=r".* Historic predictions data is required to run backtest .*"):
8585
Backtesting(bt_config)
8686

8787
Backtesting.cleanup()

tests/freqai/test_freqai_datadrawer.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test_use_strategy_to_populate_indicators(mocker, freqai_conf):
9898
shutil.rmtree(Path(freqai.dk.full_path))
9999

100100

101-
def test_get_timerange_from_backtesting_live_dataframe(mocker, freqai_conf):
101+
def test_get_timerange_from_live_historic_predictions(mocker, freqai_conf):
102102
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
103103
exchange = get_patched_exchange(mocker, freqai_conf)
104104
strategy.dp = DataProvider(freqai_conf, exchange)
@@ -115,7 +115,7 @@ def test_get_timerange_from_backtesting_live_dataframe(mocker, freqai_conf):
115115
freqai.dd.save_historic_predictions_to_disk()
116116
freqai.dd.save_global_metadata_to_disk({"start_dry_live_date": 1516406400})
117117

118-
timerange = freqai.dd.get_timerange_from_backtesting_live_dataframe()
118+
timerange = freqai.dd.get_timerange_from_live_historic_predictions()
119119
assert timerange.startts == 1516406400
120120
assert timerange.stopts == 1517356500
121121

@@ -129,4 +129,4 @@ def test_get_timerange_from_backtesting_live_df_pred_not_found(mocker, freqai_co
129129
OperationalException,
130130
match=r'Historic predictions not found.*'
131131
):
132-
freqai.dd.get_timerange_from_backtesting_live_dataframe()
132+
freqai.dd.get_timerange_from_live_historic_predictions()

0 commit comments

Comments
 (0)