# Portfolio Ingest

The portfolio ingest module (`orxaq.fabric.portfolio_ingest`) converts real bank
loan tape files (CSV or JSON) into the `LoanExposure` objects required by the
Orxaq CECL/CCAR pipeline.

## Quick start

```python
from orxaq.fabric.portfolio_ingest import ingest_portfolio

result = ingest_portfolio("path/to/fr_y14m_loans.csv")
print(f"Loaded {result.row_count} loans, {result.error_count} errors")
for loan in result.loans:
    print(loan.loan_id, loan.pd, loan.ead)
```

## CLI integration

Pass `--input-file` to `orxaq run` to use a real loan tape instead of synthetic
data:

```bash
orxaq run \
  --institution-id FIRST_NATIONAL \
  --period 2026Q1 \
  --output-dir ./filing \
  --input-file /data/fr_y14m_2026q1.csv
```

## Supported formats

| Extension | Format |
|-----------|--------|
| `.csv`    | RFC 4180 CSV with a header row (standard FR Y-14M column names by default) |
| `.json`   | JSON array of objects **or** `{"loans": [...]}` wrapper |

## Standard FR Y-14M column names

`FR_Y14M_CONFIG` (the default) expects these column names:

| Column | Required | Type | Bounds |
|--------|----------|------|--------|
| `loan_id` | ✓ | str | — |
| `fico_score` | ✓ | float | 300–850 |
| `interest_rate` | ✓ | float | 0–0.5 |
| `ltv` | ✓ | float | 0–2.0 |
| `dti` | ✓ | float | 0–1.0 |
| `pd` | optional | float | 0–1.0 |
| `lgd` | optional | float | 0–1.0 |
| `ead` | optional | float | — |
| `current_balance` | optional | float | — (fallback EAD) |
| `property_type` | optional | str | — (maps to sector) |
| `origination_date` | optional | `YYYY-MM-DD` | — (used for vintage) |
| `delinquency_status` | optional | str | — |

Optional numeric fields default to industry values when absent:
`pd=0.05`, `lgd=0.45`, `ead` derived from `current_balance` or 0.0.

## Custom field mapping

Banks with non-standard column names can supply a `PortfolioIngestConfig`:

```python
from orxaq.fabric.portfolio_ingest import PortfolioIngestConfig, ingest_portfolio

cfg = PortfolioIngestConfig(
    field_map={
        "AcctNum":     "loan_id",
        "Rate":        "interest_rate",
        "CreditScore": "fico_score",
        "LTV_Ratio":   "ltv",
        "DTI_Ratio":   "dti",
    },
    required_fields=["AcctNum", "Rate", "CreditScore", "LTV_Ratio", "DTI_Ratio"],
    optional_fields=[],
    bounds={"CreditScore": (300.0, 850.0)},
)
result = ingest_portfolio("proprietary_tape.csv", cfg)
```

## Quality gates

By default `ingest_portfolio` requires ≥ 95 % of rows to parse cleanly.
Override the threshold or disable the gate:

```python
# Stricter gate
result = ingest_portfolio(path, quality_threshold=0.99)

# Disable gate (e.g. exploratory analysis)
result = ingest_portfolio(path, run_quality=False)
```

## Audit trail

Pass an `audit_log` list to capture an entry for compliance systems:

```python
log: list = []
result = ingest_portfolio(path, audit_log=log)
# log[0] == {"action": "portfolio_ingest", "actor": "system",
#             "details": {"path": "...", "row_count": 20, "fields": [...]}}
```

## API reference

### `ingest_portfolio(path, config=None, audit_log=None, *, run_quality=True, quality_threshold=0.95) → IngestResult`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `path` | `str \| Path` | required | Loan tape file path |
| `config` | `PortfolioIngestConfig \| None` | `FR_Y14M_CONFIG` | Field mapping and validation config |
| `audit_log` | `list \| None` | `None` | Mutable list to append audit entry to |
| `run_quality` | `bool` | `True` | Enable quality gate |
| `quality_threshold` | `float` | `0.95` | Minimum fraction of valid rows |

**Returns** `IngestResult(loans, row_count, error_count, warnings)`

**Raises** `FileNotFoundError` if the file does not exist; `ValueError` on
missing required fields, out-of-bounds values, or quality gate failure.

### `PortfolioIngestConfig`

```python
@dataclass
class PortfolioIngestConfig:
    field_map: dict[str, str]          # source_col → canonical_name
    required_fields: list[str]         # source column names
    optional_fields: list[str]         # source column names
    bounds: dict[str, tuple[float, float]]  # source_col → (min, max)
    date_format: str = "%Y-%m-%d"
    default_sector: str = "unknown"
```

### `IngestResult`

```python
@dataclass
class IngestResult:
    loans: list[LoanExposure]
    row_count: int
    error_count: int
    warnings: list[str]
```
