Source code for kaxanuk.attribution_analysis.input_handlers.csv_portfolio_input_handler

"""
CSV portfolio input handler for reading portfolio allocation files.

Read portfolio weight files in CSV format, supporting both horizontal
(Ticker | Date1 | Date2 | ...) and vertical (Date | Ticker1 | Ticker2 | ...)
layouts with automatic format detection and transposition.
"""

from pathlib import Path
import csv as python_csv
import logging

import pyarrow as pa
from pyarrow import csv
import pyarrow.compute as pc

from kaxanuk.attribution_analysis.exceptions import MissingPortfolioError, ReadingCsvPortfolioError
from kaxanuk.attribution_analysis.input_handlers import input_handlers_helpers
from kaxanuk.attribution_analysis.portfolio_data_pipeline import portfolio_transformer
from kaxanuk.attribution_analysis.input_handlers.portfolio_input_handler_interface import PortfolioInputHandlerInterface

MAX_PRECISION = 38
PRECISION = 72

logger = logging.getLogger(__name__)

[docs] class CsvPortfolioInputHandler(PortfolioInputHandlerInterface): """ Handles reading and parsing of CSV portfolio files. This class reads a portfolio CSV file, infers decimal precision dynamically for numeric columns, and converts them into a typed PyArrow table suitable for financial pipelines. Supports both horizontal and vertical formats: - Horizontal: Ticker | Date1 | Date2 | ... - Vertical: Date | Ticker1 | Ticker2 | ... (auto-transposes to horizontal) Attributes ---------- base_dir : Path Directory where portfolio CSV files are located. """ def __init__( self, base_dir: str ) -> None: """ Initialize the CSV portfolio handler. Parameters ---------- base_dir : str Base directory containing portfolio CSV files. """ self.base_dir = Path(base_dir)
[docs] def read_portfolio( self, *, file_name: str ) -> pa.Table: """ Reads a portfolio from a CSV file and converts it into a typed PyArrow Table. Automatically detects horizontal vs vertical format and returns horizontal format. Parameters ---------- file_name : str Name of the CSV file (with or without .csv extension). Returns ------- pa.Table Processed PyArrow table containing portfolio tickers and weights in horizontal format. Raises ------ FileNotFoundError If the file does not exist in the specified base directory. ReadingCsvPortfolioError If the CSV cannot be read or processed correctly. """ file_path = ( self.base_dir / file_name if file_name.lower().endswith(".csv") else self.base_dir / f"{file_name}.csv" ) if not file_path.exists(): raise MissingPortfolioError( f"Portfolio file not found in {self.base_dir}: {file_path.name}" ) new_columns: list[pa.ChunkedArray] = [] csv_column_names: list[str] = [] try: # Detect format with file_path.open() as f: reader = python_csv.reader(f) headers = next(reader) first_col = headers[0].strip() format_type = portfolio_transformer.detect_portfolio_format( first_column_name=first_col, error_class=ReadingCsvPortfolioError ) logger.info("Detected %s format for %s", format_type, file_path.name) if format_type == "vertical": with file_path.open() as f: reader = python_csv.reader(f) headers = next(reader) # Force first column (Date) to be string, rest can be anything column_types = {headers[0]: pa.string()} for header in headers[1:]: column_types[header] = pa.string() convert_options = csv.ConvertOptions(column_types=column_types) prem_table = csv.read_csv(file_path, convert_options=convert_options) # Transpose to horizontal format prem_table = portfolio_transformer.transpose_vertical_to_horizontal(table=prem_table) else: # Horizontal format - read with all string types convert_options = csv.ConvertOptions( column_types={h.strip(): pa.string() for h in headers} ) prem_table = csv.read_csv(file_path, convert_options=convert_options) # Normalize first column to "Ticker" (case-insensitive) if prem_table.column_names[0].lower() == "ticker": new_names = ["Ticker", *prem_table.column_names[1:]] prem_table = prem_table.rename_columns(new_names) # Guard: first column must be "Ticker" after normalization if prem_table.column_names[0] != "Ticker": raise ReadingCsvPortfolioError( f"First column of portfolio must be named 'Ticker' " f"(found: '{prem_table.column_names[0]}')" ) for column_name in prem_table.column_names: csv_column_names.append(column_name) col = prem_table[column_name] if column_name == "Ticker": new_columns.append(col) else: decimal_spec = input_handlers_helpers.infer_decimal_precision_and_scale(col=col) target_type = input_handlers_helpers.get_decimal_type( precision=decimal_spec.precision, scale=decimal_spec.scale, precision_limit=PRECISION ) new_columns.append(pc.cast(col, target_type, safe=False)) return pa.table(new_columns, names=csv_column_names) except ReadingCsvPortfolioError: raise except FileNotFoundError as error: raise ReadingCsvPortfolioError( f"Portfolio file not found: {file_path}" ) from error except pa.lib.ArrowInvalid as error: raise ReadingCsvPortfolioError( f"Invalid CSV format in portfolio file {file_path.name}: {error}" ) from error except (ValueError, KeyError, TypeError) as error: raise ReadingCsvPortfolioError( f"Error parsing portfolio file {file_path.name}: {error}" ) from error except OSError as error: raise ReadingCsvPortfolioError( f"I/O error reading portfolio file {file_path.name}: {error}" ) from error