""" Utility functions for dexorder. Includes timestamp conversions, date parsing, and other common utilities. All internal timestamps use nanoseconds since epoch (UTC). """ import logging import re from typing import Union from datetime import datetime, timezone import pandas as pd from dateutil import parser as _dateutil_parser from dateutil.relativedelta import relativedelta log = logging.getLogger(__name__) # Type alias for flexible timestamp input TimestampInput = Union[int, float, str, datetime, pd.Timestamp] NANOS_PER_SECOND = 1_000_000_000 _RELATIVE_RE = re.compile( r'^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$', re.IGNORECASE, ) def _parse_relative_date(s: str) -> datetime | None: """Parse relative date strings like '30 days ago', '2 weeks ago'.""" m = _RELATIVE_RE.match(s.strip()) if not m: return None n, unit = int(m.group(1)), m.group(2).lower() return datetime.now(timezone.utc) - relativedelta(**{f'{unit}s': n}) def to_nanoseconds(timestamp: TimestampInput) -> int: """ Convert various timestamp formats to nanoseconds since epoch. This is the canonical way to convert user-friendly timestamps (unix seconds, date strings, datetime objects) into the internal nanosecond format used throughout the dexorder system. Args: timestamp: Can be: - Unix timestamp (int/float) - assumed to be in seconds - ISO date string (str) - parsed using dateutil - Relative date string (str) - e.g. "30 days ago", "2 weeks ago" - datetime object - pandas Timestamp Returns: Nanoseconds since epoch as integer Examples: >>> to_nanoseconds(1640000000) # Unix timestamp in seconds 1640000000000000000 >>> to_nanoseconds(1640000000.5) # Unix timestamp with fractional seconds 1640000000500000000 >>> to_nanoseconds("2021-12-20") 1639958400000000000 """ if isinstance(timestamp, (int, float)): return int(timestamp * NANOS_PER_SECOND) elif isinstance(timestamp, str): dt = _parse_relative_date(timestamp) if dt is None: dt = _dateutil_parser.parse(timestamp) if dt is None: raise ValueError(f"Could not parse date string: {timestamp}") return int(dt.timestamp() * NANOS_PER_SECOND) elif isinstance(timestamp, datetime): return int(timestamp.timestamp() * NANOS_PER_SECOND) elif isinstance(timestamp, pd.Timestamp): return int(timestamp.timestamp() * NANOS_PER_SECOND) else: raise TypeError(f"Unsupported timestamp type: {type(timestamp)}") def to_seconds(timestamp_nanos: int) -> float: """ Convert nanoseconds since epoch to Unix timestamp in seconds. Args: timestamp_nanos: Timestamp in nanoseconds since epoch Returns: Unix timestamp in seconds (float) Examples: >>> to_seconds(1640000000000000000) 1640000000.0 """ return timestamp_nanos / NANOS_PER_SECOND def to_datetime(timestamp_nanos: int) -> datetime: """ Convert nanoseconds since epoch to datetime object (UTC). Args: timestamp_nanos: Timestamp in nanoseconds since epoch Returns: datetime object in UTC """ return datetime.fromtimestamp(timestamp_nanos / NANOS_PER_SECOND) def to_timestamp(timestamp_nanos: int) -> pd.Timestamp: """ Convert nanoseconds since epoch to pandas Timestamp. Args: timestamp_nanos: Timestamp in nanoseconds since epoch Returns: pandas Timestamp """ return pd.Timestamp(timestamp_nanos, unit='ns')