freqtrade_origin/freqtrade/configuration/timerange.py

180 lines
6.2 KiB
Python
Raw Normal View History

"""
This module contains the argument manager class
"""
2024-05-12 14:29:24 +00:00
import logging
import re
from datetime import datetime, timezone
2023-06-23 16:15:06 +00:00
from typing_extensions import Self
2022-11-10 17:26:14 +00:00
from freqtrade.constants import DATETIME_PRINT_FORMAT
2024-03-19 06:08:05 +00:00
from freqtrade.exceptions import ConfigurationError
2020-02-02 04:00:40 +00:00
logger = logging.getLogger(__name__)
2019-09-12 09:13:20 +00:00
class TimeRange:
"""
object defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used.
if *type is None, don't use corresponding startvalue.
"""
2024-05-12 14:29:24 +00:00
def __init__(
self,
starttype: str | None = None,
stoptype: str | None = None,
2024-05-12 14:29:24 +00:00
startts: int = 0,
stopts: int = 0,
):
self.starttype: str | None = starttype
self.stoptype: str | None = stoptype
self.startts: int = startts
self.stopts: int = stopts
@property
def startdt(self) -> datetime | None:
if self.startts:
return datetime.fromtimestamp(self.startts, tz=timezone.utc)
return None
@property
def stopdt(self) -> datetime | None:
if self.stopts:
return datetime.fromtimestamp(self.stopts, tz=timezone.utc)
return None
2022-11-10 17:26:14 +00:00
@property
def timerange_str(self) -> str:
"""
Returns a string representation of the timerange as used by parse_timerange.
Follows the format yyyymmdd-yyyymmdd - leaving out the parts that are not set.
"""
2024-05-12 14:29:24 +00:00
start = ""
stop = ""
2022-11-10 17:26:14 +00:00
if startdt := self.startdt:
2024-05-12 14:29:24 +00:00
start = startdt.strftime("%Y%m%d")
2022-11-10 17:26:14 +00:00
if stopdt := self.stopdt:
2024-05-12 14:29:24 +00:00
stop = stopdt.strftime("%Y%m%d")
2022-11-10 17:26:14 +00:00
return f"{start}-{stop}"
@property
def start_fmt(self) -> str:
"""
Returns a string representation of the start date
"""
2024-05-12 14:29:24 +00:00
val = "unbounded"
2022-11-10 17:26:14 +00:00
if (startdt := self.startdt) is not None:
val = startdt.strftime(DATETIME_PRINT_FORMAT)
return val
@property
def stop_fmt(self) -> str:
"""
Returns a string representation of the stop date
"""
2024-05-12 14:29:24 +00:00
val = "unbounded"
2022-11-10 17:26:14 +00:00
if (stopdt := self.stopdt) is not None:
val = stopdt.strftime(DATETIME_PRINT_FORMAT)
return val
2019-08-14 08:22:54 +00:00
def __eq__(self, other):
"""Override the default Equals behavior"""
2024-05-12 14:29:24 +00:00
return (
self.starttype == other.starttype
and self.stoptype == other.stoptype
and self.startts == other.startts
and self.stopts == other.stopts
)
2019-08-14 08:22:54 +00:00
2020-02-02 04:00:40 +00:00
def subtract_start(self, seconds: int) -> None:
"""
Subtracts <seconds> from startts if startts is set.
:param seconds: Seconds to subtract from starttime
:return: None (Modifies the object in place)
"""
2019-10-20 09:29:25 +00:00
if self.startts:
self.startts = self.startts - seconds
2024-05-12 14:29:24 +00:00
def adjust_start_if_necessary(
self, timeframe_secs: int, startup_candles: int, min_date: datetime
) -> None:
"""
Adjust startts by <startup_candles> candles.
Applies only if no startup-candles have been available.
:param timeframe_secs: Timeframe in seconds e.g. `timeframe_to_seconds('5m')`
:param startup_candles: Number of candles to move start-date forward
:param min_date: Minimum data date loaded. Key kriterium to decide if start-time
has to be moved
:return: None (Modifies the object in place)
"""
2024-05-12 14:29:24 +00:00
if not self.starttype or (startup_candles and min_date.timestamp() >= self.startts):
2019-10-28 12:05:54 +00:00
# If no startts was defined, or backtest-data starts at the defined backtest-date
2024-05-12 14:29:24 +00:00
logger.warning(
"Moving start-date by %s candles to account for startup time.", startup_candles
)
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
2024-05-12 14:29:24 +00:00
self.starttype = "date"
2023-06-23 16:15:06 +00:00
@classmethod
def parse_timerange(cls, text: str | None) -> Self:
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if not text:
2023-06-23 16:15:06 +00:00
return cls(None, None, 0, 0)
2024-05-12 14:29:24 +00:00
syntax = [
(r"^-(\d{8})$", (None, "date")),
(r"^(\d{8})-$", ("date", None)),
(r"^(\d{8})-(\d{8})$", ("date", "date")),
(r"^-(\d{10})$", (None, "date")),
(r"^(\d{10})-$", ("date", None)),
(r"^(\d{10})-(\d{10})$", ("date", "date")),
(r"^-(\d{13})$", (None, "date")),
(r"^(\d{13})-$", ("date", None)),
(r"^(\d{13})-(\d{13})$", ("date", "date")),
]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
if match: # Regex has matched
rvals = match.groups()
index = 0
start: int = 0
stop: int = 0
if stype[0]:
starts = rvals[index]
2024-05-12 14:29:24 +00:00
if stype[0] == "date" and len(starts) == 8:
start = int(
datetime.strptime(starts, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
)
elif len(starts) == 13:
start = int(starts) // 1000
else:
start = int(starts)
index += 1
if stype[1]:
stops = rvals[index]
2024-05-12 14:29:24 +00:00
if stype[1] == "date" and len(stops) == 8:
stop = int(
datetime.strptime(stops, "%Y%m%d")
.replace(tzinfo=timezone.utc)
.timestamp()
)
elif len(stops) == 13:
stop = int(stops) // 1000
else:
stop = int(stops)
if start > stop > 0:
2024-03-19 06:08:05 +00:00
raise ConfigurationError(
2024-05-12 14:29:24 +00:00
f'Start date is after stop date for timerange "{text}"'
)
2023-06-23 16:15:06 +00:00
return cls(stype[0], stype[1], start, stop)
2024-03-19 06:08:05 +00:00
raise ConfigurationError(f'Incorrect syntax for timerange "{text}"')