Source code for kaxanuk.attribution_analysis.portfolio_data_pipeline.portfolio_service
"""
Portfolio Service for loading portfolio and benchmark weight files.
Orchestrates the loading of portfolio weight files through a sequence
of input handlers (CSV, Excel), returning a raw PyArrow table.
"""
import datetime
import logging
from typing import Literal
import pyarrow as pa
from kaxanuk.attribution_analysis.exceptions import MissingPortfolioError
from kaxanuk.attribution_analysis.input_handlers.portfolio_input_handler_interface import (
PortfolioInputHandlerInterface,
)
from kaxanuk.attribution_analysis.portfolio_data_pipeline import portfolio_transformer
logger = logging.getLogger(__name__)
[docs]
class PortfolioService:
"""
Loads portfolio/benchmark weight files using available input handlers.
"""
[docs]
@staticmethod
def load_portfolio(
portfolio_handlers: list[PortfolioInputHandlerInterface],
file_name: str,
start_date: datetime.date | Literal["auto"] | None = None,
end_date: datetime.date | Literal["auto"] | None = None,
) -> pa.Table:
"""
Load a portfolio/benchmark weights file using the first handler
able to read it.
Parameters
----------
portfolio_handlers : list[PortfolioInputHandlerInterface]
Input handlers to try, in order, until one succeeds.
file_name : str
File name (without extension) of the portfolio/benchmark
weights file to load.
start_date : datetime.date or {"auto"} or None, optional
Configured start of the analysis period, typically
`Configuration.start_date`. When given (and not ``"auto"``),
the loaded table's dates must include at least one date on or
after `start_date`. Checked immediately after this (cheap)
file loads, so a misconfigured date fails fast instead of
after the heavier market data/factor files are loaded.
end_date : datetime.date or {"auto"} or None, optional
Configured end of the analysis period, checked the same way
as `start_date`.
Returns
-------
pa.Table
The raw table returned by the successful handler.
Raises
------
MissingPortfolioError
If every handler in `portfolio_handlers` fails to load
`file_name`.
DateRangeError
If `start_date`/`end_date` don't overlap any date in the
loaded table.
"""
errors: list[str] = []
for handler in portfolio_handlers:
try:
table = handler.read_portfolio(file_name=file_name)
except Exception as exc: # noqa: BLE001 - intentional: try each handler, aggregate failures
errors.append(f"{type(handler).__name__}: {exc!s}")
continue
portfolio_transformer.validate_portfolio_date_range(
table=table,
start_date=start_date,
end_date=end_date,
source_name=f"portfolio '{file_name}'",
)
logger.info(
"Loaded portfolio '%s' via %s (%d tickers)",
file_name,
type(handler).__name__,
table.num_rows,
)
return table
raise MissingPortfolioError(
f"Could not load portfolio '{file_name}':\n" + "\n".join(errors)
)