Skip to content

Commit 3903b04

Browse files
committed
save_live_data_backtest - added docs and tests
1 parent 99bff9c commit 3903b04

6 files changed

+114
-11
lines changed

docs/freqai-parameter-table.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ 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+
| `save_live_data_backtest` | Save live dataframe during dry/live runs to reuse in backtesting with [Backtest live models](freqai-running.md#backtest_live_models)) option.
1819
| `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.
1920
| `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`.
2021
| `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

+4-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ 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 models generated in dry/run for comparison or other study. For that, you must set `"purge_old_models"` to `False` in the config.
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:
85+
86+
1. Set `"save_live_data_backtest"` to `True` in the config. With this option, FreqAI will save the live dataframe for reuse in backtesting. This option requires less disk space and backtesting will run faster.
87+
2. Set `"purge_old_models"` to `False` and `"save_live_data_backtest"` 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.
8588

8689
The `--timerange` parameter must not be informed, as it will be automatically calculated through the training end dates of the models.
8790

freqtrade/freqai/data_kitchen.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -1541,14 +1541,16 @@ def save_backtesting_live_dataframe(
15411541
if self.backtesting_live_model_path.is_file():
15421542
saved_dataframe = self.get_backtesting_live_dataframe()
15431543
concat_dataframe = pd.concat([saved_dataframe, last_row_df])
1544-
concat_dataframe.reset_index(drop=True).to_feather(
1545-
self.backtesting_live_model_path, compression_level=9, compression='lz4')
1544+
self.save_backtesting_live_dataframe_to_feather(concat_dataframe)
15461545
else:
1547-
last_row_df.reset_index(drop=True).to_feather(
1548-
self.backtesting_live_model_path, compression_level=9, compression='lz4')
1546+
self.save_backtesting_live_dataframe_to_feather(last_row_df)
15491547

15501548
shutil.copy(self.backtesting_live_model_path, self.backtesting_live_model_bkp_path)
15511549

1550+
def save_backtesting_live_dataframe_to_feather(self, dataframe: DataFrame):
1551+
dataframe.reset_index(drop=True).to_feather(
1552+
self.backtesting_live_model_path, compression_level=9, compression='lz4')
1553+
15521554
def get_backtesting_live_dataframe(
15531555
self
15541556
) -> DataFrame:

freqtrade/freqai/freqai_interface.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,8 @@ def fit_live_predictions(self, dk: FreqaiDataKitchen, pair: str) -> None:
694694
for label in full_labels:
695695
if self.dd.historic_predictions[dk.pair][label].dtype == object:
696696
continue
697-
f = spy.stats.norm.fit(self.dd.historic_predictions[dk.pair][label].tail(num_candles))
697+
f = spy.stats.norm.fit(
698+
self.dd.historic_predictions[dk.pair][label].fillna(0).tail(num_candles))
698699
dk.data["labels_mean"][label], dk.data["labels_std"][label] = f[0], f[1]
699700

700701
return
@@ -882,11 +883,7 @@ def backtesting_fit_live_predictions(self, dk: FreqaiDataKitchen):
882883
if index >= fit_live_predictions_candles:
883884
self.dd.historic_predictions[self.dk.pair] = (
884885
dk.full_df.iloc[index - fit_live_predictions_candles:index])
885-
else:
886-
self.dd.historic_predictions[self.dk.pair] = dk.full_df.iloc[:index]
887-
888-
self.fit_live_predictions(self.dk, self.dk.pair)
889-
if index >= fit_live_predictions_candles:
886+
self.fit_live_predictions(self.dk, self.dk.pair)
890887
for label in label_columns:
891888
if dk.full_df[label].dtype == object:
892889
continue
@@ -899,6 +896,7 @@ def backtesting_fit_live_predictions(self, dk: FreqaiDataKitchen):
899896
for extra_col in self.dk.data["extra_returns_per_train"]:
900897
dk.full_df.at[index, f"{extra_col}"] = (
901898
self.dk.data["extra_returns_per_train"][extra_col])
899+
902900
return
903901

904902
# Following methods which are overridden by user made prediction models.

tests/freqai/test_freqai_datakitchen.py

+44
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,47 @@ def test_get_full_model_path(mocker, freqai_conf, model):
259259

260260
model_path = freqai.dk.get_full_models_path(freqai_conf)
261261
assert model_path.is_dir() is True
262+
263+
264+
def test_save_backtesting_live_dataframe(mocker, freqai_conf):
265+
freqai, dataframe = make_unfiltered_dataframe(mocker, freqai_conf)
266+
dataframe_without_last_candle = dataframe.copy()
267+
dataframe_without_last_candle.drop(dataframe.tail(1).index, inplace=True)
268+
freqai_conf.update({"save_live_data_backtest": True})
269+
freqai.dk.save_backtesting_live_dataframe(dataframe_without_last_candle, "ADA/BTC")
270+
saved_dataframe = freqai.dk.get_backtesting_live_dataframe()
271+
assert len(saved_dataframe) == 1
272+
assert saved_dataframe.iloc[-1, 0] == dataframe_without_last_candle.iloc[-1, 0]
273+
freqai.dk.save_backtesting_live_dataframe(dataframe, "ADA/BTC")
274+
saved_dataframe = freqai.dk.get_backtesting_live_dataframe()
275+
assert len(saved_dataframe) == 2
276+
assert saved_dataframe.iloc[-1, 0] == dataframe.iloc[-1, 0]
277+
assert saved_dataframe.iloc[-2, 0] == dataframe.iloc[-2, 0]
278+
279+
280+
def test_get_timerange_from_backtesting_live_dataframe(mocker, freqai_conf):
281+
freqai, dataframe = make_unfiltered_dataframe(mocker, freqai_conf)
282+
freqai_conf.update({"save_live_data_backtest": True})
283+
freqai.dk.set_backtesting_live_dataframe_path("ADA/BTC")
284+
freqai.dk.save_backtesting_live_dataframe_to_feather(dataframe)
285+
timerange = freqai.dk.get_timerange_from_backtesting_live_dataframe()
286+
assert timerange.startts == 1516406400
287+
assert timerange.stopts == 1517356500
288+
289+
290+
def test_get_timerange_from_backtesting_live_dataframe_folder_not_found(mocker, freqai_conf):
291+
freqai, _ = make_unfiltered_dataframe(mocker, freqai_conf)
292+
with pytest.raises(
293+
OperationalException,
294+
match=r'Saved live data not found.*'
295+
):
296+
freqai.dk.get_timerange_from_backtesting_live_dataframe()
297+
298+
299+
def test_saved_live_bt_file_not_found(mocker, freqai_conf):
300+
freqai, _ = make_unfiltered_dataframe(mocker, freqai_conf)
301+
with pytest.raises(
302+
OperationalException,
303+
match=r'.*live backtesting dataframe file not found.*'
304+
):
305+
freqai.dk.get_backtesting_live_dataframe()

tests/freqai/test_freqai_interface.py

+55
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,61 @@ def test_start_backtesting_from_existing_folder(mocker, freqai_conf, caplog):
300300
shutil.rmtree(Path(freqai.dk.full_path))
301301

302302

303+
def test_start_backtesting_from_saved_live_dataframe(mocker, freqai_conf, caplog):
304+
freqai_conf.update({"save_live_data_backtest": True})
305+
freqai_conf.update({"freqai_backtest_live_models": True})
306+
307+
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
308+
exchange = get_patched_exchange(mocker, freqai_conf)
309+
strategy.dp = DataProvider(freqai_conf, exchange)
310+
strategy.freqai_info = freqai_conf.get("freqai", {})
311+
freqai = strategy.freqai
312+
freqai.live = False
313+
freqai.dk = FreqaiDataKitchen(freqai_conf)
314+
timerange = TimeRange.parse_timerange("20180110-20180130")
315+
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
316+
sub_timerange = TimeRange.parse_timerange("20180110-20180130")
317+
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
318+
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
319+
metadata = {"pair": "ADA/BTC"}
320+
321+
# create a dummy live dataframe file with 10 rows
322+
dataframe_predictions = df.tail(10).copy()
323+
dataframe_predictions["&s_close"] = dataframe_predictions["close"] * 1.1
324+
freqai.dk.set_backtesting_live_dataframe_path("ADA/BTC")
325+
freqai.dk.save_backtesting_live_dataframe_to_feather(dataframe_predictions)
326+
327+
freqai.start_backtesting_from_live_saved_files(df, metadata, freqai.dk)
328+
assert len(freqai.dk.return_dataframe) == len(df)
329+
assert len(freqai.dk.return_dataframe[freqai.dk.return_dataframe["&s_close"] > 0]) == (
330+
len(dataframe_predictions))
331+
shutil.rmtree(Path(freqai.dk.full_path))
332+
333+
334+
def test_backtesting_fit_live_predictions(mocker, freqai_conf, caplog):
335+
freqai_conf.get("freqai", {}).update({"fit_live_predictions_candles": 10})
336+
strategy = get_patched_freqai_strategy(mocker, freqai_conf)
337+
exchange = get_patched_exchange(mocker, freqai_conf)
338+
strategy.dp = DataProvider(freqai_conf, exchange)
339+
strategy.freqai_info = freqai_conf.get("freqai", {})
340+
freqai = strategy.freqai
341+
freqai.live = False
342+
freqai.dk = FreqaiDataKitchen(freqai_conf)
343+
timerange = TimeRange.parse_timerange("20180128-20180130")
344+
freqai.dd.load_all_pair_histories(timerange, freqai.dk)
345+
sub_timerange = TimeRange.parse_timerange("20180129-20180130")
346+
corr_df, base_df = freqai.dd.get_base_and_corr_dataframes(sub_timerange, "LTC/BTC", freqai.dk)
347+
df = freqai.dk.use_strategy_to_populate_indicators(strategy, corr_df, base_df, "LTC/BTC")
348+
freqai.dk.pair = "ADA/BTC"
349+
freqai.dk.full_df = df
350+
assert "&-s_close_mean" not in freqai.dk.full_df.columns
351+
assert "&-s_close_std" not in freqai.dk.full_df.columns
352+
freqai.backtesting_fit_live_predictions(freqai.dk)
353+
assert "&-s_close_mean" in freqai.dk.full_df.columns
354+
assert "&-s_close_std" in freqai.dk.full_df.columns
355+
shutil.rmtree(Path(freqai.dk.full_path))
356+
357+
303358
def test_follow_mode(mocker, freqai_conf):
304359
freqai_conf.update({"timerange": "20180110-20180130"})
305360

0 commit comments

Comments
 (0)