2023-05-14 15:46:56 +00:00
|
|
|
from datetime import timedelta
|
2024-10-04 05:10:49 +00:00
|
|
|
from typing import NamedTuple, Optional
|
2018-10-30 19:02:01 +00:00
|
|
|
|
|
|
|
from pandas import DataFrame
|
|
|
|
|
2022-03-25 05:55:37 +00:00
|
|
|
from freqtrade.enums import ExitType
|
2019-04-09 09:27:35 +00:00
|
|
|
from freqtrade.exchange import timeframe_to_minutes
|
2023-05-14 15:46:56 +00:00
|
|
|
from freqtrade.util.datetime_helpers import dt_utc
|
2018-10-30 19:02:01 +00:00
|
|
|
|
2020-09-28 17:43:15 +00:00
|
|
|
|
2023-05-14 15:46:56 +00:00
|
|
|
tests_start_time = dt_utc(2018, 10, 3)
|
2024-05-12 13:29:14 +00:00
|
|
|
tests_timeframe = "1h"
|
2018-10-30 19:02:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
class BTrade(NamedTuple):
|
|
|
|
"""
|
|
|
|
Minimalistic Trade result used for functional backtesting
|
|
|
|
"""
|
2024-05-12 13:29:14 +00:00
|
|
|
|
2022-03-24 19:33:47 +00:00
|
|
|
exit_reason: ExitType
|
2018-10-30 19:02:01 +00:00
|
|
|
open_tick: int
|
|
|
|
close_tick: int
|
2021-09-26 13:20:59 +00:00
|
|
|
enter_tag: Optional[str] = None
|
2022-03-16 18:26:08 +00:00
|
|
|
is_short: bool = False
|
2018-10-30 19:02:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
class BTContainer(NamedTuple):
|
|
|
|
"""
|
|
|
|
Minimal BacktestContainer defining Backtest inputs and results.
|
|
|
|
"""
|
2024-05-12 13:29:14 +00:00
|
|
|
|
2024-10-04 05:09:51 +00:00
|
|
|
data: list[list[float]]
|
2018-10-30 19:02:01 +00:00
|
|
|
stop_loss: float
|
2024-10-04 05:09:51 +00:00
|
|
|
roi: dict[str, float]
|
|
|
|
trades: list[BTrade]
|
2018-10-30 19:02:01 +00:00
|
|
|
profit_perc: float
|
2019-03-17 14:28:04 +00:00
|
|
|
trailing_stop: bool = False
|
2019-06-13 17:35:20 +00:00
|
|
|
trailing_only_offset_is_reached: bool = False
|
2020-02-11 01:17:10 +00:00
|
|
|
trailing_stop_positive: Optional[float] = None
|
2019-06-13 17:35:20 +00:00
|
|
|
trailing_stop_positive_offset: float = 0.0
|
2022-04-03 17:27:30 +00:00
|
|
|
use_exit_signal: bool = False
|
2021-06-29 13:17:52 +00:00
|
|
|
use_custom_stoploss: bool = False
|
2022-02-05 14:12:29 +00:00
|
|
|
custom_entry_price: Optional[float] = None
|
2022-02-05 14:20:05 +00:00
|
|
|
custom_exit_price: Optional[float] = None
|
2021-11-30 19:42:18 +00:00
|
|
|
leverage: float = 1.0
|
2022-05-04 05:13:02 +00:00
|
|
|
timeout: Optional[int] = None
|
|
|
|
adjust_entry_price: Optional[float] = None
|
2018-10-30 19:02:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
def _get_frame_time_from_offset(offset):
|
2020-03-08 10:35:31 +00:00
|
|
|
minutes = offset * timeframe_to_minutes(tests_timeframe)
|
2023-05-14 15:46:56 +00:00
|
|
|
return tests_start_time + timedelta(minutes=minutes)
|
2018-10-30 19:02:01 +00:00
|
|
|
|
|
|
|
|
2020-03-08 10:35:31 +00:00
|
|
|
def _build_backtest_dataframe(data):
|
2024-05-12 13:29:14 +00:00
|
|
|
columns = [
|
|
|
|
"date",
|
|
|
|
"open",
|
|
|
|
"high",
|
|
|
|
"low",
|
|
|
|
"close",
|
|
|
|
"volume",
|
|
|
|
"enter_long",
|
|
|
|
"exit_long",
|
|
|
|
"enter_short",
|
|
|
|
"exit_short",
|
|
|
|
]
|
2021-08-23 19:35:01 +00:00
|
|
|
if len(data[0]) == 8:
|
|
|
|
# No short columns
|
|
|
|
data = [d + [0, 0] for d in data]
|
2024-05-12 13:29:14 +00:00
|
|
|
columns = columns + ["enter_tag"] if len(data[0]) == 11 else columns
|
2018-10-30 19:02:01 +00:00
|
|
|
|
2020-03-08 10:35:31 +00:00
|
|
|
frame = DataFrame.from_records(data, columns=columns)
|
2024-05-12 13:29:14 +00:00
|
|
|
frame["date"] = frame["date"].apply(_get_frame_time_from_offset)
|
2018-10-30 19:02:01 +00:00
|
|
|
# Ensure floats are in place
|
2024-05-12 13:29:14 +00:00
|
|
|
for column in ["open", "high", "low", "close", "volume"]:
|
|
|
|
frame[column] = frame[column].astype("float64")
|
2021-10-30 14:10:28 +00:00
|
|
|
|
|
|
|
# Ensure all candles make kindof sense
|
2024-05-12 13:29:14 +00:00
|
|
|
assert all(frame["low"] <= frame["close"])
|
|
|
|
assert all(frame["low"] <= frame["open"])
|
|
|
|
assert all(frame["high"] >= frame["close"])
|
|
|
|
assert all(frame["high"] >= frame["open"])
|
2018-10-30 19:02:01 +00:00
|
|
|
return frame
|