from datetime import timedelta from typing import NamedTuple, Optional from pandas import DataFrame from freqtrade.enums import ExitType from freqtrade.exchange import timeframe_to_minutes from freqtrade.util.datetime_helpers import dt_utc tests_start_time = dt_utc(2018, 10, 3) tests_timeframe = "1h" class BTrade(NamedTuple): """ Minimalistic Trade result used for functional backtesting """ exit_reason: ExitType open_tick: int close_tick: int enter_tag: Optional[str] = None is_short: bool = False class BTContainer(NamedTuple): """ Minimal BacktestContainer defining Backtest inputs and results. """ data: list[list[float]] stop_loss: float roi: dict[str, float] trades: list[BTrade] profit_perc: float trailing_stop: bool = False trailing_only_offset_is_reached: bool = False trailing_stop_positive: Optional[float] = None trailing_stop_positive_offset: float = 0.0 use_exit_signal: bool = False use_custom_stoploss: bool = False custom_entry_price: Optional[float] = None custom_exit_price: Optional[float] = None leverage: float = 1.0 timeout: Optional[int] = None adjust_entry_price: Optional[float] = None def _get_frame_time_from_offset(offset): minutes = offset * timeframe_to_minutes(tests_timeframe) return tests_start_time + timedelta(minutes=minutes) def _build_backtest_dataframe(data): columns = [ "date", "open", "high", "low", "close", "volume", "enter_long", "exit_long", "enter_short", "exit_short", ] if len(data[0]) == 8: # No short columns data = [d + [0, 0] for d in data] columns = columns + ["enter_tag"] if len(data[0]) == 11 else columns frame = DataFrame.from_records(data, columns=columns) frame["date"] = frame["date"].apply(_get_frame_time_from_offset) # Ensure floats are in place for column in ["open", "high", "low", "close", "volume"]: frame[column] = frame[column].astype("float64") # Ensure all candles make kindof sense assert all(frame["low"] <= frame["close"]) assert all(frame["low"] <= frame["open"]) assert all(frame["high"] >= frame["close"]) assert all(frame["high"] >= frame["open"]) return frame