"""
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)