2019-08-14 08:07:14 +00:00
|
|
|
"""
|
|
|
|
This module contains the argument manager class
|
|
|
|
"""
|
2019-10-23 17:30:01 +00:00
|
|
|
import logging
|
2019-08-14 08:07:14 +00:00
|
|
|
import re
|
2022-11-10 15:33:57 +00:00
|
|
|
from datetime import datetime, timezone
|
2019-08-14 08:07:14 +00:00
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
import arrow
|
|
|
|
|
2022-11-10 17:26:14 +00:00
|
|
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
2021-03-19 05:40:04 +00:00
|
|
|
from freqtrade.exceptions import OperationalException
|
|
|
|
|
2020-02-02 04:00:40 +00:00
|
|
|
|
2019-10-23 17:30:01 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2019-08-14 08:07:14 +00:00
|
|
|
|
2019-09-12 09:13:20 +00:00
|
|
|
class TimeRange:
|
2019-08-14 08:07:14 +00:00
|
|
|
"""
|
|
|
|
object defining timerange inputs.
|
|
|
|
[start/stop]type defines if [start/stop]ts shall be used.
|
|
|
|
if *type is None, don't use corresponding startvalue.
|
|
|
|
"""
|
|
|
|
|
2019-08-14 08:22:54 +00:00
|
|
|
def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None,
|
|
|
|
startts: int = 0, stopts: int = 0):
|
2019-08-14 08:07:14 +00:00
|
|
|
|
|
|
|
self.starttype: Optional[str] = starttype
|
|
|
|
self.stoptype: Optional[str] = stoptype
|
|
|
|
self.startts: int = startts
|
|
|
|
self.stopts: int = stopts
|
|
|
|
|
2022-11-10 15:33:57 +00:00
|
|
|
@property
|
|
|
|
def startdt(self) -> Optional[datetime]:
|
|
|
|
if self.startts:
|
|
|
|
return datetime.fromtimestamp(self.startts, tz=timezone.utc)
|
2022-11-10 17:11:39 +00:00
|
|
|
return None
|
2022-11-10 15:33:57 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def stopdt(self) -> Optional[datetime]:
|
|
|
|
if self.stopts:
|
|
|
|
return datetime.fromtimestamp(self.stopts, tz=timezone.utc)
|
2022-11-10 17:11:39 +00:00
|
|
|
return None
|
2022-11-10 15:33:57 +00:00
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
start = ''
|
|
|
|
stop = ''
|
|
|
|
if startdt := self.startdt:
|
|
|
|
start = startdt.strftime('%Y%m%d')
|
|
|
|
if stopdt := self.stopdt:
|
|
|
|
stop = stopdt.strftime('%Y%m%d')
|
|
|
|
return f"{start}-{stop}"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def start_fmt(self) -> str:
|
|
|
|
"""
|
|
|
|
Returns a string representation of the start date
|
|
|
|
"""
|
|
|
|
val = 'unbounded'
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
val = 'unbounded'
|
|
|
|
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"""
|
|
|
|
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
|
|
|
and self.startts == other.startts and self.stopts == other.stopts)
|
|
|
|
|
2020-02-02 04:00:40 +00:00
|
|
|
def subtract_start(self, seconds: int) -> None:
|
2019-10-23 17:30:01 +00:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
2019-11-03 09:01:05 +00:00
|
|
|
def adjust_start_if_necessary(self, timeframe_secs: int, startup_candles: int,
|
2021-05-06 17:34:10 +00:00
|
|
|
min_date: datetime) -> None:
|
2019-10-23 17:30:01 +00:00
|
|
|
"""
|
|
|
|
Adjust startts by <startup_candles> candles.
|
|
|
|
Applies only if no startup-candles have been available.
|
2020-03-08 10:35:31 +00:00
|
|
|
:param timeframe_secs: Timeframe in seconds e.g. `timeframe_to_seconds('5m')`
|
2019-10-23 17:30:01 +00:00
|
|
|
: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)
|
|
|
|
"""
|
|
|
|
if (not self.starttype or (startup_candles
|
2021-05-06 17:34:10 +00:00
|
|
|
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
|
2019-10-23 17:30:01 +00:00
|
|
|
logger.warning("Moving start-date by %s candles to account for startup time.",
|
|
|
|
startup_candles)
|
2021-05-06 17:34:10 +00:00
|
|
|
self.startts = int(min_date.timestamp() + timeframe_secs * startup_candles)
|
2019-10-23 17:30:01 +00:00
|
|
|
self.starttype = 'date'
|
|
|
|
|
2019-08-14 08:07:14 +00:00
|
|
|
@staticmethod
|
2020-02-02 04:00:40 +00:00
|
|
|
def parse_timerange(text: Optional[str]) -> 'TimeRange':
|
2019-08-14 08:07:14 +00:00
|
|
|
"""
|
|
|
|
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 text is None:
|
|
|
|
return TimeRange(None, None, 0, 0)
|
|
|
|
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')),
|
2019-10-19 17:38:16 +00:00
|
|
|
(r'^-(\d{13})$', (None, 'date')),
|
|
|
|
(r'^(\d{13})-$', ('date', None)),
|
2019-10-19 12:53:56 +00:00
|
|
|
(r'^(\d{13})-(\d{13})$', ('date', 'date')),
|
|
|
|
]
|
2019-08-14 08:07:14 +00:00
|
|
|
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]
|
|
|
|
if stype[0] == 'date' and len(starts) == 8:
|
2020-10-12 17:58:04 +00:00
|
|
|
start = arrow.get(starts, 'YYYYMMDD').int_timestamp
|
2019-10-19 12:53:56 +00:00
|
|
|
elif len(starts) == 13:
|
|
|
|
start = int(starts) // 1000
|
2019-08-14 08:07:14 +00:00
|
|
|
else:
|
|
|
|
start = int(starts)
|
|
|
|
index += 1
|
|
|
|
if stype[1]:
|
|
|
|
stops = rvals[index]
|
|
|
|
if stype[1] == 'date' and len(stops) == 8:
|
2020-10-12 17:58:04 +00:00
|
|
|
stop = arrow.get(stops, 'YYYYMMDD').int_timestamp
|
2019-10-19 12:53:56 +00:00
|
|
|
elif len(stops) == 13:
|
|
|
|
stop = int(stops) // 1000
|
2019-08-14 08:07:14 +00:00
|
|
|
else:
|
|
|
|
stop = int(stops)
|
2021-03-18 21:38:54 +00:00
|
|
|
if start > stop > 0:
|
2021-03-19 05:40:04 +00:00
|
|
|
raise OperationalException(
|
|
|
|
f'Start date is after stop date for timerange "{text}"')
|
2019-08-14 08:07:14 +00:00
|
|
|
return TimeRange(stype[0], stype[1], start, stop)
|
2021-03-19 05:40:04 +00:00
|
|
|
raise OperationalException(f'Incorrect syntax for timerange "{text}"')
|