"""Halving calendar and position within the halving cycle.""" import numpy as np import pandas as pd GENESIS = pd.Timestamp("2009-01-03") # Block heights 210k, 420k, 630k, 840k (UTC dates). HALVINGS = pd.DatetimeIndex(["2012-11-28", "2016-07-09", "2020-05-11", "2024-04-20"]) # Later halvings are projected at the length of the last cycle. Block times drift # by weeks per cycle, which is noise at the resolution this is used. _LAST_CYCLE = HALVINGS[-1] - HALVINGS[-2] _PROJECTED = pd.DatetimeIndex([HALVINGS[-1] + k * _LAST_CYCLE for k in range(1, 6)]) # Genesis starts cycle 0. CYCLE_STARTS = pd.DatetimeIndex([GENESIS]).append(HALVINGS).append(_PROJECTED) def cycle_position(dates) -> tuple[np.ndarray, np.ndarray]: """ For each date, return (cycle index, days since that cycle began). Cycle 0 runs from genesis to the first halving; a halving day is day 0 of the cycle it starts. """ dates = pd.DatetimeIndex(dates) if (dates < GENESIS).any(): raise ValueError("date before genesis") if (dates >= CYCLE_STARTS[-1]).any(): raise ValueError("date beyond projected halvings") index = CYCLE_STARTS.searchsorted(dates, side="right") - 1 days = (dates - CYCLE_STARTS[index]).days return np.asarray(index), np.asarray(days) def cycle_fraction(dates) -> tuple[np.ndarray, np.ndarray]: """For each date, return (cycle index, fraction of that cycle elapsed, in [0, 1)).""" index, days = cycle_position(dates) lengths = (CYCLE_STARTS[index + 1] - CYCLE_STARTS[index]).days return index, days / np.asarray(lengths)