from functools import lru_cache import datetime as dt from typing import Union import pandas as pd import pytz # Unix epoch in UTC (timezone-aware) epoch = dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc) def query_uptodate(records: pd.DataFrame, r_length_min: float) -> Union[float, None]: """ Check if records that span a period of time are up-to-date. :param records: The dataframe holding results from a query. :param r_length_min: The timespan in minutes of each record in the data. :return: timestamp - None if records are up-to-date otherwise the newest timestamp on record. """ print('\nChecking if the records are up-to-date...') # Get the newest timestamp from the records passed in stored in ms last_timestamp = float(records.time.max()) print(f'The last timestamp on record is {last_timestamp}') # Get a timestamp of the UTC time in milliseconds to match the records in the DB now_timestamp = unix_time_millis(dt.datetime.now(pytz.UTC)) print(f'The timestamp now is {now_timestamp}') # Get the seconds since the records have been updated seconds_since_update = ms_to_seconds(now_timestamp - last_timestamp) # Convert to minutes minutes_since_update = seconds_since_update / 60 print(f'The minutes since last update is {minutes_since_update}') print(f'And the length of each record is {r_length_min}') # Return the timestamp if the time since last update is more than the timespan each record covers tolerance_minutes = 10 / 60 # 10 seconds tolerance in minutes if minutes_since_update > (r_length_min - tolerance_minutes): # Return the last timestamp in seconds return last_timestamp return None def ms_to_seconds(timestamp: float) -> float: """ Converts milliseconds to seconds. :param timestamp: The timestamp in milliseconds. :return: The timestamp in seconds. """ return timestamp / 1000 def unix_time_seconds(d_time: dt.datetime) -> float: """ Converts a datetime object to Unix timestamp in seconds. :param d_time: The datetime object to convert. :return: The Unix timestamp in seconds. """ if d_time.tzinfo is None: raise ValueError("d_time is timezone naive. Please provide a timezone-aware datetime.") return (d_time - epoch).total_seconds() def unix_time_millis(d_time: dt.datetime) -> float: """ Converts a datetime object to Unix timestamp in milliseconds. :param d_time: The datetime object to convert. :return: The Unix timestamp in milliseconds. """ if d_time.tzinfo is None: raise ValueError("d_time is timezone naive. Please provide a timezone-aware datetime.") return (d_time - epoch).total_seconds() * 1000.0 def query_satisfied(start_datetime: dt.datetime, records: pd.DataFrame, r_length_min: float) -> Union[float, None]: """ Check if records span back far enough to satisfy a query. This function determines whether the records provided cover the required start_datetime. It calculates the total duration covered by the records and checks if this duration, starting from the earliest record, reaches back to include the start_datetime. :param start_datetime: The datetime the query starts at. :param records: The dataframe holding results from a query. :param r_length_min: The timespan in minutes of each record in the data. :return: None if the query is satisfied (records span back far enough), otherwise returns the earliest timestamp in records in seconds. """ print('\nChecking if the query is satisfied...') if start_datetime.tzinfo is None: raise ValueError("start_datetime is timezone naive. Please provide a timezone-aware datetime.") # Convert start_datetime to Unix timestamp in milliseconds start_timestamp = unix_time_millis(start_datetime) print(f'Start timestamp: {start_timestamp}') if records.empty: print('No records found. Query cannot be satisfied.') return float('nan') # Get the oldest timestamp from the records passed in first_timestamp = float(records.time.min()) print(f'First timestamp in records: {first_timestamp}') # Calculate the total duration of the records in milliseconds total_duration = len(records) * (r_length_min * 60 * 1000) print(f'Total duration of records: {total_duration}') # Check if the first timestamp plus the total duration is greater than or equal to the start timestamp if start_timestamp <= first_timestamp + total_duration: return None return first_timestamp @lru_cache(maxsize=500) def ts_of_n_minutes_ago(n: int, candle_length: float) -> dt.datetime: """ Returns the approximate datetime for the start of a candle that was 'n' candles ago. :param n: The number of candles ago to calculate. :param candle_length: The length of each candle in minutes. :return: The approximate datetime for the start of the 'n'-th candle ago. """ # Increment 'n' by 1 to ensure we account for the time that has passed since the last candle closed. n += 1 # Calculate the total minutes ago the 'n'-th candle started. minutes_ago = n * candle_length # Get the current UTC datetime. now = dt.datetime.now(pytz.UTC) # Calculate the datetime for 'n' candles ago. date_of = now - dt.timedelta(minutes=minutes_ago) # Return the calculated datetime. return date_of @lru_cache(maxsize=20) def timeframe_to_minutes(timeframe: str) -> int: """ Converts a string representing a timeframe into an integer representing the approximate minutes. :param timeframe: Timeframe format is [multiplier:focus]. e.g., '15m', '4h', '1d' :return: Minutes the timeframe represents, e.g., '2h' -> 120 (minutes). """ # Extract the numerical part of the timeframe param. digits = int("".join([i if i.isdigit() else "" for i in timeframe])) # Extract the alpha part of the timeframe param. letter = "".join([i if i.isalpha() else "" for i in timeframe]) if letter == 'm': pass elif letter == 'h': digits *= 60 elif letter == 'd': digits *= 60 * 24 elif letter == 'w': digits *= 60 * 24 * 7 elif letter == 'M': digits *= 60 * 24 * 31 # Maximum number of days in a month elif letter == 'Y': digits *= 60 * 24 * 365 # Exact number of days in a year return digits