Skip to content

Commit 1f21d5e

Browse files
authored
Merge pull request freqtrade#12126 from stash86/main-stash
Improve lookahead analysis to use full dataframe comparison instead of just the last row
2 parents 765a0b5 + a531f86 commit 1f21d5e

File tree

2 files changed

+34
-34
lines changed

2 files changed

+34
-34
lines changed

docs/lookahead-analysis.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Many strategies, without the programmer knowing, have fallen prey to lookahead b
3838
This typically makes the strategy backtest look profitable, sometimes to extremes, but this is not realistic as the strategy is "cheating" by looking at data it would not have in dry or live modes.
3939

4040
The reason why strategies can "cheat" is because the freqtrade backtesting process populates the full dataframe including all candle timestamps at the outset.
41-
If the programmer is not careful or oblivious how things work internally
41+
If the programmer is not careful or oblivious how things work internally
4242
(which sometimes can be really hard to find out) then the strategy will look into the future.
4343

4444
This command is made to try to verify the validity in the form of the aforementioned lookahead bias.
@@ -50,8 +50,7 @@ After this initial backtest runs, it will look if the `minimum-trade-amount` is
5050
If this happens, use a wider timerange to get more trades for the analysis, or use a timerange where more trades occur.
5151

5252
After setting the baseline it will then do additional backtest runs for every entry and exit separately.
53-
When these verification backtests complete, it will compare the indicators at the signal candles (both entry or exit)
54-
and report the bias.
53+
When these verification backtests complete, it will compare both dataframes (baseline and sliced) for any difference in columns' value and report the bias.
5554
After all signals have been verified or falsified a result table will be generated for the user to see.
5655

5756
### How to find and remove bias? How can I salvage a biased strategy?
@@ -98,8 +97,8 @@ If the strategy has many different signals / signal types, it's up to you to sel
9897
This would lead to a false-negative, i.e. the strategy will be reported as non-biased.
9998
- `lookahead-analysis` has access to the same backtesting options and this can introduce problems.
10099
Please don't use any options like enabling position stacking as this will distort the number of checked signals.
101-
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
100+
If you decide to do so, then make doubly sure that you won't ever run out of `max_open_trades` slots,
102101
and that you have enough capital in the backtest wallet configuration.
103-
- In the results table, the `biased_indicators` column
102+
- In the results table, the `biased_indicators` column
104103
will falsely flag FreqAI target indicators defined in `set_freqai_targets()` as biased.
105104
**These are not biased and can safely be ignored.**

freqtrade/optimize/analysis/lookahead.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -70,34 +70,29 @@ def analyze_indicators(self, full_vars: VarHolder, cut_vars: VarHolder, current_
7070
cut_df: DataFrame = cut_vars.indicators[current_pair]
7171
full_df: DataFrame = full_vars.indicators[current_pair]
7272

73-
# cut longer dataframe to length of the shorter
74-
full_df_cut = full_df[(full_df.date == cut_vars.compared_dt)].reset_index(drop=True)
75-
cut_df_cut = cut_df[(cut_df.date == cut_vars.compared_dt)].reset_index(drop=True)
76-
77-
# check if dataframes are not empty
78-
if full_df_cut.shape[0] != 0 and cut_df_cut.shape[0] != 0:
79-
# compare dataframes
80-
compare_df = full_df_cut.compare(cut_df_cut)
81-
82-
if compare_df.shape[0] > 0:
83-
for col_name, values in compare_df.items():
84-
col_idx = compare_df.columns.get_loc(col_name)
85-
compare_df_row = compare_df.iloc[0]
86-
# compare_df now comprises tuples with [1] having either 'self' or 'other'
87-
if "other" in col_name[1]:
88-
continue
89-
self_value = compare_df_row.iloc[col_idx]
90-
other_value = compare_df_row.iloc[col_idx + 1]
91-
92-
# output differences
93-
if self_value != other_value:
94-
if not self.current_analysis.false_indicators.__contains__(col_name[0]):
95-
self.current_analysis.false_indicators.append(col_name[0])
96-
logger.info(
97-
f"=> found look ahead bias in indicator "
98-
f"{col_name[0]}. "
99-
f"{str(self_value)} != {str(other_value)}"
100-
)
73+
# trim full_df to the same index and length as cut_df
74+
cut_full_df = full_df.loc[cut_df.index]
75+
compare_df = cut_full_df.compare(cut_df)
76+
77+
if compare_df.shape[0] > 0:
78+
for col_name in compare_df:
79+
col_idx = compare_df.columns.get_loc(col_name)
80+
compare_df_row = compare_df.iloc[0]
81+
# compare_df now comprises tuples with [1] having either 'self' or 'other'
82+
if "other" in col_name[1]:
83+
continue
84+
self_value = compare_df_row.iloc[col_idx]
85+
other_value = compare_df_row.iloc[col_idx + 1]
86+
87+
# output differences
88+
if self_value != other_value:
89+
if not self.current_analysis.false_indicators.__contains__(col_name[0]):
90+
self.current_analysis.false_indicators.append(col_name[0])
91+
logger.info(
92+
f"=> found look ahead bias in column "
93+
f"{col_name[0]}. "
94+
f"{str(self_value)} != {str(other_value)}"
95+
)
10196

10297
def prepare_data(self, varholder: VarHolder, pairs_to_load: list[DataFrame]):
10398
if "freqai" in self.local_config and "identifier" in self.local_config["freqai"]:
@@ -132,7 +127,13 @@ def prepare_data(self, varholder: VarHolder, pairs_to_load: list[DataFrame]):
132127
varholder.data, varholder.timerange = backtesting.load_bt_data()
133128
varholder.timeframe = backtesting.timeframe
134129

135-
varholder.indicators = backtesting.strategy.advise_all_indicators(varholder.data)
130+
temp_indicators = backtesting.strategy.advise_all_indicators(varholder.data)
131+
filled_indicators = dict()
132+
for pair, dataframe in temp_indicators.items():
133+
filled_indicators[pair] = backtesting.strategy.ft_advise_signals(
134+
dataframe, {"pair": pair}
135+
)
136+
varholder.indicators = filled_indicators
136137
varholder.result = self.get_result(backtesting, varholder.indicators)
137138

138139
def fill_entry_and_exit_varHolders(self, result_row):

0 commit comments

Comments
 (0)