Skip to content

Series

Time-series construction for the metrics pipeline. This module builds the CanonicalSeries -- the foundational data structure from which all metrics are derived.

Canonical Series

The three core series are:

  • daily_gains: Sum of gain_normalized per day, indexed by date.
  • cumulative_gains: Running cumulative sum of daily_gains.
  • drawdowns: cumulative_gains - cumulative_gains.cummax() (always <= 0).

If the input DataFrame contains an is_supply column, supply-only and demand-only breakdowns are computed in parallel.

Rolling Periods

The rolling_period_sums function computes rolling-window sums over the daily gains series, used by the calculator for worst-N-day loss and rolling return metrics.

Canonical Series - Time series construction for metrics computation.

Classes

CanonicalSeries dataclass

CanonicalSeries(daily_gains: Series, cumulative_gains: Series, drawdowns: Series, daily_gains_supply: Series | None = None, daily_gains_demand: Series | None = None, cumulative_gains_supply: Series | None = None, cumulative_gains_demand: Series | None = None, drawdowns_supply: Series | None = None, drawdowns_demand: Series | None = None)

Container for canonical daily time series used in metrics computation.

Attributes: daily_gains: Series indexed by date with sum of gain_normalized per day cumulative_gains: Cumulative sum of daily_gains drawdowns: cumulative_gains - cumulative_gains.cummax() (always <= 0) daily_gains_supply: Optional series for supply (is_supply=True) gains daily_gains_demand: Optional series for demand (is_supply=False) gains cumulative_gains_supply: Optional cumulative sum of supply gains cumulative_gains_demand: Optional cumulative sum of demand gains drawdowns_supply: Optional drawdowns for supply drawdowns_demand: Optional drawdowns for demand

Functions

build_canonical_series

build_canonical_series(df_std_filtered: DataFrame) -> CanonicalSeries

Build canonical time series from a filtered standardized DataFrame.

Args: df_std_filtered: Standardized DataFrame filtered to cleared trades only

Returns: CanonicalSeries with daily_gains, cumulative_gains, drawdowns, and optional supply/demand breakdown if 'is_supply' column exists

Steps: 1. Group by 'date', sum 'gain_normalized' -> daily_gains 2. Sort by date 3. cumulative_gains = daily_gains.cumsum() 4. drawdowns = cumulative_gains - cumulative_gains.cummax() 5. If 'is_supply' exists, compute supply/demand breakdown

Source code in src/progridpy/metrics/series.py
def build_canonical_series(df_std_filtered: pd.DataFrame) -> CanonicalSeries:
    """
    Build canonical time series from a filtered standardized DataFrame.

    Args:
        df_std_filtered: Standardized DataFrame filtered to cleared trades only

    Returns:
        CanonicalSeries with daily_gains, cumulative_gains, drawdowns,
        and optional supply/demand breakdown if 'is_supply' column exists

    Steps:
        1. Group by 'date', sum 'gain_normalized' -> daily_gains
        2. Sort by date
        3. cumulative_gains = daily_gains.cumsum()
        4. drawdowns = cumulative_gains - cumulative_gains.cummax()
        5. If 'is_supply' exists, compute supply/demand breakdown
    """
    # Group by date, sum gain_normalized
    daily_gains = df_std_filtered.groupby("date")["gain_normalized"].sum()
    daily_gains = daily_gains.sort_index()

    # Convert index to DatetimeIndex for rolling operations
    daily_gains.index = pd.to_datetime(daily_gains.index)

    # Cumulative and drawdowns
    cumulative_gains = daily_gains.cumsum()
    drawdowns = cumulative_gains - cumulative_gains.cummax()

    # Supply/demand breakdown
    daily_gains_supply = None
    daily_gains_demand = None
    cumulative_gains_supply = None
    cumulative_gains_demand = None
    drawdowns_supply = None
    drawdowns_demand = None

    if "is_supply" in df_std_filtered.columns:
        # Supply trades (is_supply=True)
        supply_df = df_std_filtered[df_std_filtered["is_supply"]]
        if len(supply_df) > 0:
            daily_gains_supply = supply_df.groupby("date")["gain_normalized"].sum()
            daily_gains_supply = daily_gains_supply.sort_index()
            daily_gains_supply.index = pd.to_datetime(daily_gains_supply.index)
            # Reindex to match the full date range, filling missing dates with 0
            daily_gains_supply = daily_gains_supply.reindex(daily_gains.index, fill_value=0.0)
            cumulative_gains_supply = daily_gains_supply.cumsum()
            drawdowns_supply = cumulative_gains_supply - cumulative_gains_supply.cummax()

        # Demand trades (is_supply=False)
        demand_df = df_std_filtered[~df_std_filtered["is_supply"]]
        if len(demand_df) > 0:
            daily_gains_demand = demand_df.groupby("date")["gain_normalized"].sum()
            daily_gains_demand = daily_gains_demand.sort_index()
            daily_gains_demand.index = pd.to_datetime(daily_gains_demand.index)
            # Reindex to match the full date range, filling missing dates with 0
            daily_gains_demand = daily_gains_demand.reindex(daily_gains.index, fill_value=0.0)
            cumulative_gains_demand = daily_gains_demand.cumsum()
            drawdowns_demand = cumulative_gains_demand - cumulative_gains_demand.cummax()

    return CanonicalSeries(
        daily_gains=daily_gains,
        cumulative_gains=cumulative_gains,
        drawdowns=drawdowns,
        daily_gains_supply=daily_gains_supply,
        daily_gains_demand=daily_gains_demand,
        cumulative_gains_supply=cumulative_gains_supply,
        cumulative_gains_demand=cumulative_gains_demand,
        drawdowns_supply=drawdowns_supply,
        drawdowns_demand=drawdowns_demand,
    )

rolling_period_sums

rolling_period_sums(daily_gains: Series, window_days: int) -> Series

Compute rolling sums over a specified window.

Args: daily_gains: Series indexed by date window_days: Number of days in the rolling window

Returns: Series with rolling sums, index aligned to window end date. NaN values are present for the first (window_days - 1) entries.

Note: Uses min_periods=window_days to ensure full windows only.

Source code in src/progridpy/metrics/series.py
def rolling_period_sums(daily_gains: pd.Series, window_days: int) -> pd.Series:
    """
    Compute rolling sums over a specified window.

    Args:
        daily_gains: Series indexed by date
        window_days: Number of days in the rolling window

    Returns:
        Series with rolling sums, index aligned to window end date.
        NaN values are present for the first (window_days - 1) entries.

    Note:
        Uses min_periods=window_days to ensure full windows only.
    """
    return daily_gains.rolling(window_days, min_periods=window_days).sum()