Source code for kaxanuk.attribution_analysis.entities.configuration

"""
Dataclass encapsulating the user's selected configuration.

Configuration instances are typically built by config handlers (e.g.
`kaxanuk.attribution_analysis.config_handlers.excel_configurator`) from
user-supplied settings, and consumed by the attribution engine to locate
input files and select processing options.
"""

import dataclasses
import datetime
from typing import Literal

from kaxanuk.attribution_analysis.exceptions import ConfigurationError

# Type alias for date configuration: either an explicit date or "auto" to derive from portfolio
type DateConfig = datetime.date | Literal["auto"]


[docs] @dataclasses.dataclass(frozen=True, slots=True) class Configuration: """ Immutable, validated configuration for a single attribution analysis run. All fields are validated in `__post_init__`, which raises :class:`~kaxanuk.attribution_analysis.exceptions.ConfigurationError` on the first invalid field found. `start_date` and `end_date` are additionally normalized from :class:`datetime.datetime` to :class:`datetime.date` if needed. Parameters ---------- input_directory : str Base directory containing market data input files. weights_portfolio_directory : str Directory containing portfolio weights files. weights_benchmark_directory : str Directory containing benchmark weights files. benchmark_returns_directory : str Directory containing benchmark returns files. investable_assets_directory : str Directory containing investable assets market data files. factor_returns_by_factor_directory : str Directory containing per-factor returns files, one file per factor. portfolio_file_name : str File name (without extension) of the portfolio weights file, in `weights_portfolio_directory`. benchmark_file_name : str File name (without extension) of the benchmark weights file, in `weights_benchmark_directory`. benchmark_return_file_name : str File name (without extension) of the benchmark returns file, in `benchmark_returns_directory`. user_column_date : str Name of the date column as it appears in the user's input files. user_column_price : str Name of the price column as it appears in the user's input files. market_data_input_format : {"csv", "parquet"} File format of the market data input files. portfolio_input_format : {"csv", "excel"} File format of the portfolio/benchmark weights input files. start_date : datetime.date or datetime.datetime or {"auto"} Start date of the analysis period, or ``"auto"`` to derive it from the portfolio data. ``datetime.datetime`` values are normalized to ``datetime.date`` after validation. end_date : datetime.date or datetime.datetime or {"auto"} End date of the analysis period, or ``"auto"`` to derive it from the portfolio data. ``datetime.datetime`` values are normalized to ``datetime.date`` after validation. brinson_fachler_method : bool Whether to run the Brinson-Fachler attribution method. factor_model_method : bool Whether to run the factor model attribution method. Raises ------ ConfigurationError If any field fails validation (see `__post_init__`). """ # Routes input_directory: str weights_portfolio_directory: str weights_benchmark_directory: str benchmark_returns_directory: str investable_assets_directory: str factor_returns_by_factor_directory: str # File names portfolio_file_name: str benchmark_file_name: str benchmark_return_file_name: str # Column mappings user_column_date: str user_column_price: str # Input format market_data_input_format: str portfolio_input_format: str # Date range start_date: DateConfig end_date: DateConfig # Methods brinson_fachler_method: bool factor_model_method: bool def __post_init__(self) -> None: """ Validate and normalize all configuration fields. Raises ------ ConfigurationError If any route, file name, or column mapping field isn't a str; if `market_data_input_format` isn't ``"csv"`` or ``"parquet"``; if `portfolio_input_format` isn't ``"csv"`` or ``"excel"``; if `start_date` or `end_date` is neither ``"auto"`` nor a ``datetime.date``/``datetime.datetime``; or if `brinson_fachler_method` or `factor_model_method` isn't a bool. """ # Routes validation if not isinstance(self.input_directory, str): raise ConfigurationError( "Incorrect format for input_directory, expecting a str" ) if not isinstance(self.weights_portfolio_directory, str): raise ConfigurationError( "Incorrect format for weights_portfolio_directory, expecting a str" ) if not isinstance(self.weights_benchmark_directory, str): raise ConfigurationError( "Incorrect format for weights_benchmark_directory, expecting a str" ) if not isinstance(self.benchmark_returns_directory, str): raise ConfigurationError( "Incorrect format for benchmark_returns_directory, expecting a str" ) if not isinstance(self.investable_assets_directory, str): raise ConfigurationError( "Incorrect format for investable_assets_directory, expecting a str" ) if not isinstance(self.factor_returns_by_factor_directory, str): raise ConfigurationError( "Incorrect format for factor_returns_by_factor_directory, expecting a str" ) # Column mappings validation if not isinstance(self.user_column_date, str): raise ConfigurationError( "Incorrect format for user_column_date, expecting a str" ) if not isinstance(self.user_column_price, str): raise ConfigurationError( "Incorrect format for user_column_price, expecting a str" ) # Input format validation if self.market_data_input_format not in ("csv", "parquet"): raise ConfigurationError( "Incorrect value for market_data_input_format, expecting 'csv' or 'parquet'" ) if self.portfolio_input_format not in ("csv", "excel"): raise ConfigurationError( "Incorrect value for portfolio_input_format, expecting 'csv' or 'excel'" ) # Date range validation and normalization if self.start_date != "auto": if not isinstance(self.start_date, (datetime.date, datetime.datetime)): raise ConfigurationError( "Incorrect Configuration.start_date type; " "expecting datetime.date, datetime.datetime, or 'auto'" ) if isinstance(self.start_date, datetime.datetime): object.__setattr__(self, 'start_date', self.start_date.date()) if self.end_date != "auto": if not isinstance(self.end_date, (datetime.date, datetime.datetime)): raise ConfigurationError( "Incorrect Configuration.end_date type; " "expecting datetime.date, datetime.datetime, or 'auto'" ) if isinstance(self.end_date, datetime.datetime): object.__setattr__(self, 'end_date', self.end_date.date()) # File names validation if not isinstance(self.portfolio_file_name, str): raise ConfigurationError( "Incorrect format for portfolio_file_name, expecting a str" ) if not isinstance(self.benchmark_file_name, str): raise ConfigurationError( "Incorrect format for benchmark_file_name, expecting a str" ) if not isinstance(self.benchmark_return_file_name, str): raise ConfigurationError( "Incorrect format for benchmark_return_file_name, expecting a str" ) # Methods validation if not isinstance(self.brinson_fachler_method, bool): raise ConfigurationError( "Incorrect format for brinson_fachler_method, expecting a bool" ) if not isinstance(self.factor_model_method, bool): raise ConfigurationError( "Incorrect format for factor_model_method, expecting a bool" ) def __getitem__(self, item: str) -> object: """ Access a configuration field by name, dict-style. Parameters ---------- item : str Name of the field to retrieve (must match one of the dataclass's field names). Returns ------- object The value of the requested field. Raises ------ AttributeError If `item` doesn't match any field name. """ return getattr(self, item)