Salta al contenuto principale

07 - Compliance Engineering: Solvency II e IFRS 17 per Platform Builder

Compliance Engineering: Solvency II e IFRS 17 per Platform Builder Pochi ambiti della tecnologia sono altrettanto complessi e ad alto rischio quanto la...

Cosa ne pensi di questo articolo?

07 - Compliance Engineering: Solvency II e IFRS 17 per Platform Builder
InsurTechFoto:Danny MenesesPexels

In Breve

Compliance Engineering: Solvency II e IFRS 17 per Platform Builder Pochi ambiti della tecnologia sono altrettanto complessi e ad alto rischio quanto la...

Compliance Engineering: Solvency II e IFRS 17 per Platform Builder

Pochi ambiti della tecnologia sono altrettanto complessi e ad alto rischio quanto la compliance regolamentare assicurativa. Solvency II e IFRS 17 sono i due frameworks normativi che governano rispettivamente la solvibilita (quanto capitale detenere) e il reporting finanziario (come contabilizzare i contratti assicurativi) per le compagnie europee. Sbagliare l'implementazione tecnica di questi framework significa sanzioni, restrizioni operative e perdita di reputazione.

La sfida per i developer e che questi frameworks non sono stati progettati con la tecnologia in mente: sono normative attuariali e contabili che devono essere tradotte in sistemi software. La maggior parte delle compagnie ha lottato con implementazioni basate su fogli Excel e batch notturni che durano ore. La buona notizia e che l'architettura moderna - data warehouse columnar, pipeline ELT, calcolo distribuito - permette finalmente di costruire sistemi di compliance scalabili e auditabili.

Secondo PwC, la compliance Solvency II e IFRS 17 presenta un'opportunità unica: le due normative condividono molti dati di input. Integrare i pipeline di reporting invece di mantenerli separati può ridurre del 40-60% il costo operativo annuo della compliance.

Cosa Imparerai

  • Panoramica tecnica di Solvency II: Pillar 1 (SCR), Pillar 2 (ORSA), Pillar 3 (reporting)
  • Data model IFRS 17: gruppi di contratti, modelli di misurazione (GMM, PAA, VFA)
  • Architettura del data warehouse per compliance assicurativa
  • Pipeline di calcolo SCR con Python e dbt
  • Costruzione dei report Solvency II (QRT) in formato XBRL
  • IFRS 17 data model e calcolo dei liability measures
  • Integrazione Solvency II + IFRS 17: dati condivisi e riduzione della duplicazione

Solvency II: Panoramica Tecnica per Developer

Solvency II e il framework europeo di solvibilita assicurativa (Direttiva 2009/138/CE), strutturato su tre pilastri:

  • Pillar 1 - Requisiti Quantitativi: calcolo del Solvency Capital Requirement (SCR), del Minimum Capital Requirement (MCR) e delle riserve tecniche (Best Estimate + Risk Margin)
  • Pillar 2 - Governance e Risk Management: ORSA (Own Risk and Solvency Assessment), sistema di controllo interno, funzioni chiave
  • Pillar 3 - Reporting e Trasparenza: QRT (Quantitative Reporting Templates) per EIOPA, SFCR (Solvency and Financial Condition Report) pubblico, RSR per il supervisore

Per un platform builder, i punti di lavoro principali sono: la pipeline di calcolo delle riserve tecniche e dello SCR (Pillar 1), l'infrastruttura dati per il reporting QRT (Pillar 3) e i sistemi di audit trail per il Pillar 2.

Componenti Dati di Solvency II

Componente Descrizione Frequenza Calcolo Output Tecnico
Best Estimate Liability (BEL) Valore attuale atteso dei flussi futuri di cassa Trimestrale / annuale Tabelle con cashflow per anno/linea
Risk Margin (RM) Costo del capitale per rischi non hedgiabili Trimestrale / annuale Valore scalare per linea di business
SCR (Standard Formula) Capitale richiesto per shock su 16 moduli di rischio Annuale (YE), semestrale Matrice di correlazione + aggregazione
QRT (Quantitative Reporting Templates) Template EIOPA per reporting regolamentare Trimestrale + annuale XBRL, Excel template EIOPA
ORSA Report Assessment proprio del rischio e della solvibilita Annuale Documento PDF + dati di supporto

Architettura del Data Warehouse per Compliance

La compliance assicurativa richiede un data warehouse con caratteristiche specifiche: storicizzazione completa (audit trail), tracciabilita di ogni trasformazione, riconciliazione tra sistemi diversi, e capacità di riprocessare periodi passati quando i dati vengono corretti (late amendments).

-- ============================================================
-- Schema dbt per Solvency II + IFRS 17 Data Warehouse
-- ============================================================

-- Layer 1: Raw / Staging (dati grezzi dai sistemi operativi)

-- Tabella base contratti assicurativi
CREATE TABLE staging.insurance_contracts (
    contract_id         VARCHAR(50) NOT NULL,
    policy_number       VARCHAR(30) NOT NULL,
    product_code        VARCHAR(20) NOT NULL,
    line_of_business    VARCHAR(30) NOT NULL,  -- auto, property, liability, life
    inception_date      DATE NOT NULL,
    expiry_date         DATE,
    issue_date          DATE NOT NULL,
    policyholder_id     VARCHAR(50),
    sum_insured         DECIMAL(18, 2),
    annual_premium      DECIMAL(18, 2),
    currency            CHAR(3) NOT NULL,
    status              VARCHAR(20),           -- active, lapsed, expired, cancelled
    -- Audit columns
    source_system       VARCHAR(30),
    load_timestamp      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    valid_from          DATE NOT NULL,
    valid_to            DATE,                  -- NULL = record corrente
    is_current          BOOLEAN DEFAULT TRUE,
    PRIMARY KEY (contract_id, valid_from)
);

-- Tabella sinistri per BEL cashflow
CREATE TABLE staging.claims (
    claim_id            VARCHAR(50) NOT NULL PRIMARY KEY,
    contract_id         VARCHAR(50) NOT NULL,
    fnol_date           DATE NOT NULL,
    incident_date       DATE NOT NULL,
    reported_amount     DECIMAL(18, 2),
    paid_amount         DECIMAL(18, 2),
    reserve_amount      DECIMAL(18, 2),       -- riserva corrente
    case_reserve        DECIMAL(18, 2),       -- riserva per sinistro specifico
    ibnr_reserve        DECIMAL(18, 2),       -- riserva IBNR
    settlement_date     DATE,
    status              VARCHAR(20),
    load_timestamp      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Tabella yield curve per sconto BEL (EIOPA Risk-Free Rate)
CREATE TABLE staging.eiopa_yield_curve (
    curve_date          DATE NOT NULL,
    currency            CHAR(3) NOT NULL,
    maturity_years      INTEGER NOT NULL,     -- 1, 2, ..., 150
    spot_rate           DECIMAL(10, 6),       -- tasso spot risk-free
    forward_rate        DECIMAL(10, 6),
    source              VARCHAR(30),          -- EIOPA pubblicazione ufficiale
    PRIMARY KEY (curve_date, currency, maturity_years)
);


-- Layer 2: Intermediate / Mart (trasformazioni dbt)

-- Calcolo cashflow proiettati per linea di business
-- Modello semplificato: in produzione usare modelli attuariali complessi
CREATE TABLE mart.solvency2_bel_cashflows (
    valuation_date      DATE NOT NULL,
    line_of_business    VARCHAR(30) NOT NULL,
    projection_year     INTEGER NOT NULL,    -- anni futuri: 1, 2, ..., N
    currency            CHAR(3) NOT NULL,
    -- Cashflow per categoria
    expected_claims_paid    DECIMAL(18, 2),  -- sinistri attesi da pagare
    expected_expenses       DECIMAL(18, 2),  -- spese di gestione attese
    expected_premiums       DECIMAL(18, 2),  -- premi futuri attesi (rami vita)
    net_cashflow            DECIMAL(18, 2),  -- cashflow netto (claims + exp - premi)
    -- Fattore di sconto dalla yield curve EIOPA
    discount_factor         DECIMAL(10, 6),
    present_value_net_cf    DECIMAL(18, 2),  -- PV del cashflow netto
    -- Metadata
    calc_run_id             VARCHAR(36),     -- UUID del run di calcolo
    calc_timestamp          TIMESTAMP,
    actuary_model_version   VARCHAR(20),
    PRIMARY KEY (valuation_date, line_of_business, projection_year, currency)
);

-- Best Estimate Liability aggregata
CREATE TABLE mart.solvency2_bel_summary (
    valuation_date      DATE NOT NULL,
    line_of_business    VARCHAR(30) NOT NULL,
    currency            CHAR(3) NOT NULL,
    best_estimate       DECIMAL(18, 2),      -- somma PV cashflow futuri
    risk_margin         DECIMAL(18, 2),      -- costo del capitale rischi non hedgiabili
    technical_provision DECIMAL(18, 2),      -- BEL + Risk Margin
    -- Componenti BEL
    bel_claims          DECIMAL(18, 2),
    bel_expenses        DECIMAL(18, 2),
    bel_premiums        DECIMAL(18, 2),
    -- Confronto con periodo precedente
    prior_quarter_bel   DECIMAL(18, 2),
    bel_movement        DECIMAL(18, 2),
    -- Metadata
    calc_run_id         VARCHAR(36),
    approved_by         VARCHAR(100),
    approval_date       DATE,
    PRIMARY KEY (valuation_date, line_of_business, currency)
);

Pipeline di Calcolo Best Estimate con Python

Il calcolo del Best Estimate Liability (BEL) e il cuore attuariale di Solvency II. In termini tecnici, richiede di proiettare i cashflow futuri attesi (sinistri, spese, premi) e scontarli con la yield curve EIOPA risk-free appropriata. Il codice seguente implementa una versione semplificata del calcolo BEL per il ramo danni.

import numpy as np
import pandas as pd
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
from datetime import date
import uuid


@dataclass
class BELInputData:
    """Dati di input per il calcolo del BEL."""
    valuation_date: date
    line_of_business: str
    currency: str
    # Triangolo di sviluppo sinistri (per Chain-Ladder o BF)
    claims_triangle: pd.DataFrame  # rows=accident_year, cols=development_year
    # Premi di competenza e spese storici
    earned_premiums: pd.Series    # indexed by year
    expense_ratio: float          # % premi
    # Yield curve EIOPA
    yield_curve: pd.Series        # indexed by maturity (1, 2, ..., 150)
    # Parametri del modello
    tail_factor: float = 1.05     # fattore di coda per il triangolo
    projection_years: int = 25    # anni di proiezione


@dataclass
class BELResult:
    """Risultato del calcolo BEL."""
    valuation_date: date
    line_of_business: str
    currency: str
    best_estimate: float
    bel_claims: float
    bel_expenses: float
    cashflow_by_year: pd.DataFrame
    calc_run_id: str
    calc_timestamp: str


class BestEstimateLiabilityCalculator:
    """
    Calcolo del Best Estimate Liability per ramo danni (Solvency II).

    Implementa il metodo Chain-Ladder per la proiezione dei sinistri
    e il discounting con la curva risk-free EIOPA.

    NOTA: Questo e un modello semplificato a scopo didattico.
    In produzione, il calcolo attuariale richiede modelli certificati
    e validati dal team attuariale.
    """

    def calculate(self, data: BELInputData) -> BELResult:
        """Esegue il calcolo completo del BEL."""
        calc_run_id = str(uuid.uuid4())

        # Step 1: Proiezione dei sinistri con Chain-Ladder
        projected_claims = self._chain_ladder_projection(data)

        # Step 2: Proiezione cashflow annuali
        cashflow_df = self._build_cashflow_projections(data, projected_claims)

        # Step 3: Sconto con yield curve EIOPA
        cashflow_df = self._apply_discounting(cashflow_df, data.yield_curve)

        # Step 4: Aggregazione
        bel_claims = float(cashflow_df["pv_claims"].sum())
        bel_expenses = float(cashflow_df["pv_expenses"].sum())
        best_estimate = bel_claims + bel_expenses

        return BELResult(
            valuation_date=data.valuation_date,
            line_of_business=data.line_of_business,
            currency=data.currency,
            best_estimate=round(best_estimate, 2),
            bel_claims=round(bel_claims, 2),
            bel_expenses=round(bel_expenses, 2),
            cashflow_by_year=cashflow_df,
            calc_run_id=calc_run_id,
            calc_timestamp=pd.Timestamp.now().isoformat(),
        )

    def _chain_ladder_projection(self, data: BELInputData) -> pd.DataFrame:
        """
        Proiezione dei sinistri con il metodo Chain-Ladder.

        Il triangolo di sviluppo ha:
        - Righe: anni di accadimento (accident year)
        - Colonne: anni di sviluppo (development year 1, 2, ..., N)
        """
        triangle = data.claims_triangle.copy()

        # Calcola i fattori di sviluppo (link ratios) dalla diagonale
        n_dev_years = len(triangle.columns)
        development_factors = []

        for j in range(n_dev_years - 1):
            col_curr = triangle.columns[j]
            col_next = triangle.columns[j + 1]
            # Solo le righe con dati in entrambe le colonne
            mask = triangle[col_curr].notna() & triangle[col_next].notna()
            if mask.sum() == 0:
                development_factors.append(1.0)
                continue
            factor = (
                triangle.loc[mask, col_next].sum() /
                triangle.loc[mask, col_curr].sum()
            )
            development_factors.append(factor)

        # Aggiungi il tail factor per l'ultimo anno di sviluppo
        development_factors.append(data.tail_factor)

        # Completa il triangolo proiettando i valori mancanti
        for i, accident_year in enumerate(triangle.index):
            for j, dev_year in enumerate(triangle.columns):
                if pd.isna(triangle.loc[accident_year, dev_year]):
                    # Proietta dalla cella precedente
                    prev_col = triangle.columns[j - 1]
                    if not pd.isna(triangle.loc[accident_year, prev_col]):
                        triangle.loc[accident_year, dev_year] = (
                            triangle.loc[accident_year, prev_col] * development_factors[j - 1]
                        )

        # Calcola i sinistri da sviluppare per anno
        # (ultima colonna del triangolo completato - ultima diagonale)
        ultimate_claims = triangle.iloc[:, -1]
        last_known = triangle.apply(lambda row: row.dropna().iloc[-1] if row.notna().any() else 0, axis=1)
        ibnr_by_year = ultimate_claims - last_known

        return pd.DataFrame({
            "accident_year": triangle.index,
            "ultimate": ultimate_claims.values,
            "last_known": last_known.values,
            "ibnr": ibnr_by_year.values,
        }).set_index("accident_year")

    def _build_cashflow_projections(
        self, data: BELInputData, projected_claims: pd.DataFrame
    ) -> pd.DataFrame:
        """
        Costruisce il profilo temporale dei cashflow futuri.
        Distribuisce i sinistri proiettati negli anni futuri.
        """
        rows = []
        total_ibnr = projected_claims["ibnr"].sum()
        avg_premium = data.earned_premiums.mean() if not data.earned_premiums.empty else 0

        for year in range(1, data.projection_years + 1):
            # Pattern di pagamento semplificato: esponenziale decrescente
            payment_weight = np.exp(-0.3 * year)
            normalizer = sum(np.exp(-0.3 * y) for y in range(1, data.projection_years + 1))
            year_claims = total_ibnr * (payment_weight / normalizer)

            year_expenses = year_claims * data.expense_ratio

            rows.append({
                "projection_year": year,
                "claims_cashflow": round(year_claims, 2),
                "expense_cashflow": round(year_expenses, 2),
                "net_cashflow": round(year_claims + year_expenses, 2),
            })

        return pd.DataFrame(rows).set_index("projection_year")

    def _apply_discounting(
        self, cashflows: pd.DataFrame, yield_curve: pd.Series
    ) -> pd.DataFrame:
        """Applica il discounting con la curva risk-free EIOPA."""
        result = cashflows.copy()
        pv_claims = []
        pv_expenses = []

        for year in cashflows.index:
            # Tasso spot per la maturity corrispondente
            rate = float(yield_curve.get(year, yield_curve.iloc[-1]))
            discount_factor = 1.0 / (1.0 + rate) ** year
            pv_claims.append(cashflows.loc[year, "claims_cashflow"] * discount_factor)
            pv_expenses.append(cashflows.loc[year, "expense_cashflow"] * discount_factor)

        result["pv_claims"] = pv_claims
        result["pv_expenses"] = pv_expenses
        result["pv_net"] = result["pv_claims"] + result["pv_expenses"]

        return result

IFRS 17: Data Model e Implementazione

IFRS 17 (in vigore dal 1 gennaio 2023 per le compagnie EU) rivoluziona la contabilita dei contratti assicurativi. Il principio cardine e che i contratti assicurativi non siano più contabilizzati quando i premi sono incassati, ma che venga riconosciuta la profittabilita futura attesa (Contractual Service Margin - CSM) e che i sinistri siano misurati al costo attuale (Current Value, non costo storico).

I tre modelli di misurazione principali di IFRS 17 sono:

  • General Measurement Model (GMM): modello principale; calcola BEL (attualizzato) + Risk Adjustment + CSM
  • Premium Allocation Approach (PAA): semplificazione per contratti breve termine (massimo 1 anno), simile al precedente IFRS 4
  • Variable Fee Approach (VFA): per contratti vita con partecipazione agli utili
import pandas as pd
import numpy as np
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from datetime import date
from enum import Enum


class IFRS17MeasurementModel(str, Enum):
    GMM = "GMM"    # General Measurement Model
    PAA = "PAA"    # Premium Allocation Approach
    VFA = "VFA"    # Variable Fee Approach


class ContractGroup(str, Enum):
    """IFRS 17 richiede la separazione in 3 gruppi di redditivita."""
    ONEROUS = "onerous"               # contratti in perdita
    NO_SIGNIFICANT_RISK = "no_significant_risk"  # nessun rischio di diventare onerosi
    REMAINING = "remaining"           # tutti gli altri


@dataclass
class IFRS17ContractGroup:
    """
    Gruppo di contratti IFRS 17.

    IFRS 17 richiede di raggruppare i contratti per:
    - Anno di emissione (cohort annuale)
    - Linea di business
    - Gruppo di redditivita (onerous/remaining/no-significant-risk)

    I gruppi NON possono essere mescolati tra anni diversi.
    """
    group_id: str
    line_of_business: str
    issue_cohort_year: int
    profitability_group: ContractGroup
    measurement_model: IFRS17MeasurementModel
    currency: str
    # Contratti nel gruppo
    contract_ids: List[str] = field(default_factory=list)


@dataclass
class IFRS17LiabilityMeasures:
    """
    Misure delle passivita IFRS 17 per un gruppo di contratti.
    GMM: LRC = FCF (BEL + RA) + CSM
    """
    group_id: str
    valuation_date: date
    measurement_model: IFRS17MeasurementModel

    # Fulfilment Cash Flows (FCF)
    best_estimate_liability: float     # BEL scontato (come Solvency II)
    risk_adjustment: float             # aggiustamento per rischio non-finanziario
    fulfilment_cash_flows: float       # = BEL + RA

    # Contractual Service Margin (CSM)
    # Profitto futuro atteso ancora da riconoscere
    csm_opening: float                 # CSM inizio periodo
    csm_accretion: float               # interessi maturati
    csm_experience_adjustments: float  # rettifiche per esperienza
    csm_release: float                 # CSM rilasciato a P&L nel periodo
    csm_closing: float                 # CSM fine periodo

    # Liability for Remaining Coverage (LRC)
    lrc: float   # = FCF + CSM (se > 0) oppure FCF (se CSM < 0, onerous)

    # Liability for Incurred Claims (LIC)
    lic: float   # per sinistri già occorsi ma non ancora liquidati

    # Total Insurance Contract Liabilities
    total_liability: float  # = LRC + LIC


class IFRS17Calculator:
    """
    Calcolatore delle misure IFRS 17.

    Implementa il GMM (General Measurement Model) e il PAA
    (Premium Allocation Approach) per contratti breve termine.
    """

    RISK_ADJUSTMENT_CONFIDENCE = 0.75  # confidenza target per RA (tipicamente 70-80%)

    def calculate_gmm(
        self,
        group: IFRS17ContractGroup,
        bel: float,
        bel_prior: float,
        risk_adjustment: float,
        csm_opening: float,
        discount_rate: float,
        coverage_units_current: float,
        coverage_units_remaining: float,
        experience_variance: float = 0.0,
    ) -> IFRS17LiabilityMeasures:
        """
        Calcola le misure IFRS 17 con il GMM.

        Args:
            bel: Best Estimate Liability corrente (attualizzato)
            bel_prior: BEL al periodo precedente
            risk_adjustment: RA al periodo corrente
            csm_opening: CSM di apertura del periodo
            discount_rate: tasso di interesse locked-in (tasso alla data di emissione)
            coverage_units_current: unita di copertura del periodo corrente
            coverage_units_remaining: unita di copertura residue
            experience_variance: varianza di esperienza sul BEL
        """
        # Fulfilment Cash Flows
        fcf = bel + risk_adjustment

        # CSM movement
        csm_accretion = csm_opening * discount_rate

        # Aggiustamento CSM per variazioni delle stime future (non experience)
        # Le experience variances vanno a P&L, non al CSM
        csm_after_accretion = csm_opening + csm_accretion

        # Rilascio CSM: proporzionale alle coverage units del periodo vs totali
        if (coverage_units_current + coverage_units_remaining) > 0:
            release_ratio = coverage_units_current / (
                coverage_units_current + coverage_units_remaining
            )
        else:
            release_ratio = 0.0

        csm_release = csm_after_accretion * release_ratio
        csm_closing = max(0.0, csm_after_accretion - csm_release)

        # Se il gruppo diventa oneroso (CSM negativo), impatta subito P&L
        if csm_after_accretion < 0:
            # Loss component: ammontare per cui il gruppo e oneroso
            csm_closing = 0.0

        # LRC = FCF + CSM (contratti non onerosi)
        lrc = fcf + csm_closing

        # LIC approssimato (in produzione: calcolo separato per sinistri incorsi)
        lic = abs(bel * 0.15)  # stima semplificata: 15% BEL e LIC

        return IFRS17LiabilityMeasures(
            group_id=group.group_id,
            valuation_date=date.today(),
            measurement_model=IFRS17MeasurementModel.GMM,
            best_estimate_liability=round(bel, 2),
            risk_adjustment=round(risk_adjustment, 2),
            fulfilment_cash_flows=round(fcf, 2),
            csm_opening=round(csm_opening, 2),
            csm_accretion=round(csm_accretion, 2),
            csm_experience_adjustments=round(experience_variance, 2),
            csm_release=round(csm_release, 2),
            csm_closing=round(csm_closing, 2),
            lrc=round(lrc, 2),
            lic=round(lic, 2),
            total_liability=round(lrc + lic, 2),
        )

    def calculate_paa(
        self,
        group: IFRS17ContractGroup,
        unearned_premium_reserve: float,
        acquisition_costs_deferred: float,
        claims_liability: float,
        risk_adjustment_incurred: float,
    ) -> IFRS17LiabilityMeasures:
        """
        Calcola le misure IFRS 17 con il PAA (contratti <= 1 anno).

        Nel PAA la LRC e approssimata dalla riserva premi non guadagnati
        meno i costi di acquisizione differiti.
        """
        lrc = unearned_premium_reserve - acquisition_costs_deferred
        lic = claims_liability + risk_adjustment_incurred

        return IFRS17LiabilityMeasures(
            group_id=group.group_id,
            valuation_date=date.today(),
            measurement_model=IFRS17MeasurementModel.PAA,
            best_estimate_liability=claims_liability,
            risk_adjustment=risk_adjustment_incurred,
            fulfilment_cash_flows=claims_liability + risk_adjustment_incurred,
            csm_opening=0.0,     # PAA non ha CSM esplicito
            csm_accretion=0.0,
            csm_experience_adjustments=0.0,
            csm_release=0.0,
            csm_closing=0.0,
            lrc=round(lrc, 2),
            lic=round(lic, 2),
            total_liability=round(lrc + lic, 2),
        )

Generazione QRT XBRL per Solvency II

I Quantitative Reporting Templates (QRT) sono i template standardizzati EIOPA che le compagnie devono trasmettere all'autorita di vigilanza (in Italia: IVASS) con frequenza trimestrale e annuale. Dal 2016 il formato di trasmissione obbligatorio e XBRL (eXtensible Business Reporting Language). La generazione automatica dei QRT da un data warehouse e uno dei casi d'uso più impattanti per la compliance engineering.

import pandas as pd
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
from datetime import date
import xml.etree.ElementTree as ET
from xml.dom import minidom


# Namespace XBRL standard per Solvency II (EIOPA)
XBRL_NAMESPACES = {
    "xbrli": "http://www.xbrl.org/2003/instance",
    "link": "http://www.xbrl.org/2003/linkbase",
    "xlink": "http://www.w3.org/1999/xlink",
    "xsi": "http://www.w3.org/2001/XMLSchema-instance",
    "s2md_met": "http://eiopa.europa.eu/xbrl/s2md/dict/met",
    "s2c_dim": "http://eiopa.europa.eu/xbrl/s2c/dict/dim",
    "s2c_CA": "http://eiopa.europa.eu/xbrl/s2c/dict/dom/CA",
    "iso4217": "http://www.xbrl.org/2003/iso4217",
}

# Template S.01.01 - Contenuto della presentazione (indice dei QRT)
# Template S.02.01 - Stato patrimoniale
# Template S.17.01 - Riserve tecniche ramo non vita
# Template S.25.01 - Solvency Capital Requirement (Standard Formula)


@dataclass
class QRTContext:
    """Metadati per la generazione del QRT XBRL."""
    entity_id: str           # codice identificativo IVASS/LEI
    entity_name: str
    reporting_period_end: date
    reporting_currency: str  # EUR per la maggior parte
    solo_or_group: str       # "solo" o "group"
    report_type: str         # "annual" o "quarterly"


class SolvencyIIQRTGenerator:
    """
    Generatore di QRT XBRL per Solvency II.

    Implementa un sottoinsieme dei template EIOPA:
    - S.01.01 (indice)
    - S.02.01 (stato patrimoniale Solvency II)
    - S.17.01 (riserve tecniche non vita)

    NOTA: In produzione, usare librerie XBRL specializzate come
    Arelle o soluzioni certified EIOPA per la compliance completa.
    """

    def generate_s01_01(self, ctx: QRTContext) -> str:
        """
        Genera il template S.01.01 - Contenuto della presentazione.
        Elenca i QRT inclusi nella submission.
        """
        root = ET.Element("xbrl", attrib={
            "xmlns": "http://www.xbrl.org/2003/instance",
            "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
        })

        # Context
        context = ET.SubElement(root, "context", id="ctx_S0101")
        entity_el = ET.SubElement(context, "entity")
        identifier = ET.SubElement(entity_el, "identifier", scheme="http://www.lei.org")
        identifier.text = ctx.entity_id
        period = ET.SubElement(context, "period")
        instant = ET.SubElement(period, "instant")
        instant.text = ctx.reporting_period_end.isoformat()

        # Unit (EUR)
        unit = ET.SubElement(root, "unit", id="EUR")
        measure = ET.SubElement(unit, "measure")
        measure.text = "iso4217:EUR"

        # Template S.01.01 data points
        # R0010: Template S.01.01 presente
        self._add_fact(root, "s2md_met:ei_S0101R0010C0010", "ctx_S0101", "EUR", "true")
        # R0020: Template S.02.01 presente (stato patrimoniale)
        self._add_fact(root, "s2md_met:ei_S0101R0020C0010", "ctx_S0101", "EUR", "true")
        # R0080: Template S.17.01 presente (riserve non vita)
        self._add_fact(root, "s2md_met:ei_S0101R0080C0010", "ctx_S0101", "EUR", "true")
        # R0190: Template S.25.01 presente (SCR formula standard)
        self._add_fact(root, "s2md_met:ei_S0101R0190C0010", "ctx_S0101", "EUR", "true")

        return self._pretty_print(root)

    def generate_s17_01(
        self,
        ctx: QRTContext,
        bel_data: Dict[str, float],
        risk_margin_data: Dict[str, float],
    ) -> str:
        """
        Genera S.17.01 - Riserve tecniche ramo non vita.

        Args:
            bel_data: BEL per linea EIOPA (es. {"motor_vehicle_liability": 1500000.0})
            risk_margin_data: Risk Margin per linea EIOPA
        """
        root = ET.Element("xbrl", attrib={
            "xmlns": "http://www.xbrl.org/2003/instance",
        })

        # Mapping linee di business interne -> codici EIOPA QRT
        lob_codes = {
            "motor_vehicle_liability": "s2c_CA:x1",
            "other_motor":             "s2c_CA:x2",
            "marine":                  "s2c_CA:x5",
            "fire_property":           "s2c_CA:x7",
            "general_liability":       "s2c_CA:x9",
            "credit_suretyship":       "s2c_CA:x10",
        }

        for lob_internal, bel_value in bel_data.items():
            lob_code = lob_codes.get(lob_internal, "s2c_CA:x99")
            ctx_id = f"ctx_S1701_{lob_internal}"

            # Context con dimensione linea di business
            context = ET.SubElement(root, "context", id=ctx_id)
            entity_el = ET.SubElement(context, "entity")
            identifier = ET.SubElement(entity_el, "identifier", scheme="http://www.lei.org")
            identifier.text = ctx.entity_id
            period = ET.SubElement(context, "period")
            instant = ET.SubElement(period, "instant")
            instant.text = ctx.reporting_period_end.isoformat()
            scenario = ET.SubElement(context, "scenario")
            explicit_member = ET.SubElement(
                scenario, "explicitMember",
                dimension="s2c_dim:LB"
            )
            explicit_member.text = lob_code

            # Unit
            unit = ET.SubElement(root, "unit", id=f"EUR_{lob_internal}")
            measure = ET.SubElement(unit, "measure")
            measure.text = "iso4217:EUR"

            # Dati QRT S.17.01
            # R0010: Riserve premi - BEL
            self._add_fact(
                root, "s2md_met:tp_S1701R0010C0010",
                ctx_id, f"EUR_{lob_internal}",
                str(round(bel_value * 0.3, 2))  # stima: 30% del BEL e premi
            )
            # R0020: Riserve sinistri - BEL
            self._add_fact(
                root, "s2md_met:tp_S1701R0020C0010",
                ctx_id, f"EUR_{lob_internal}",
                str(round(bel_value * 0.7, 2))  # 70% del BEL e sinistri
            )
            # R0060: Best Estimate (totale)
            self._add_fact(
                root, "s2md_met:tp_S1701R0060C0010",
                ctx_id, f"EUR_{lob_internal}",
                str(round(bel_value, 2))
            )
            # R0070: Risk Margin
            rm = risk_margin_data.get(lob_internal, 0.0)
            self._add_fact(
                root, "s2md_met:tp_S1701R0070C0010",
                ctx_id, f"EUR_{lob_internal}",
                str(round(rm, 2))
            )
            # R0100: Technical Provisions totale (BEL + RM)
            self._add_fact(
                root, "s2md_met:tp_S1701R0100C0010",
                ctx_id, f"EUR_{lob_internal}",
                str(round(bel_value + rm, 2))
            )

        return self._pretty_print(root)

    def _add_fact(
        self, parent: ET.Element,
        concept: str, context_ref: str,
        unit_ref: str, value: str
    ) -> ET.Element:
        """Aggiunge un data point XBRL."""
        fact = ET.SubElement(parent, concept, attrib={
            "contextRef": context_ref,
            "unitRef": unit_ref,
            "decimals": "2",
        })
        fact.text = value
        return fact

    def _pretty_print(self, root: ET.Element) -> str:
        xml_str = ET.tostring(root, encoding="unicode")
        dom = minidom.parseString(xml_str)
        return dom.toprettyxml(indent="  ", encoding=None)

Integrazione Solvency II + IFRS 17: Dati Condivisi

La grande opportunità per ridurre il costo di compliance e che Solvency II e IFRS 17 usano molti dati in comune: entrambi richiedono il Best Estimate delle passivita (anche se calcolato con metodologie diverse), entrambi richiedono una yield curve per il discounting (anche se diversa: EIOPA risk-free per Solvency II, tasso di sconto corrente per IFRS 17), entrambi segmentano per linea di business.

Dati Condivisi Solvency II / IFRS 17

Dato Solvency II IFRS 17 Differenza Chiave
Best Estimate Liability BEL (risk-free discounted) FCF - BEL component Yield curve diversa, definizione cashflow diversa
Yield Curve EIOPA risk-free + VA/MA Current + locked-in rate Due curve separate in IFRS 17
Segmentazione LoBs 17 linee EIOPA Linee più aggregate Mappatura necessaria
Triangoli di sviluppo Per Chain-Ladder Per FCF Stesso dato sorgente
Dati polizze/contratti Per calcolo riserve Per gruppi di contratti Stesso dato sorgente, diversa aggregazione

Best Practices e Anti-pattern

Best Practices per Compliance Engineering

  • Single Source of Truth per i dati di input: sinistri, polizze e premi devono provenire da un unico data warehouse certificato; calcoli Solvency II e IFRS 17 devono partire dagli stessi dati grezzi
  • Audit trail immutabile per ogni run: ogni esecuzione del calcolo deve produrre un log completo con: versione del codice, dati di input, parametri, output - necessario per le revisioni attuariali e regolamentari
  • Separazione dati vs calcolo: il data warehouse deve contenere solo dati storici e curati; la logica di calcolo deve essere nel layer applicativo (Python/R), non in stored procedure SQL
  • Testing attuariale obbligatorio: ogni modificaa ai modelli di calcolo deve essere accompagnata da validazione del team attuariale con confronto vs periodo precedente e analisi degli scostamenti
  • Automazione del ciclo di chiusura: il processo di calcolo, validazione e trasmissione dei QRT deve essere completamente automatizzato con trigger temporali e alert per SLA

Anti-pattern da Evitare

  • Excel come sistema di calcolo in produzione: fogli Excel non sono auditabili, non sono scalabili e non supportano il controllo versione; ogni calcolo regolamentare deve essere in codice versionato
  • Dati separati per Solvency II e IFRS 17: mantenere due pipeline separate duplica costi e rischi; la duplicazione e causa di riconciliazioni costose tra i due framework
  • Hardcodare la yield curve: la curva risk-free EIOPA cambia ogni mese; deve essere caricata automaticamente dal sito EIOPA, non inserita manualmente
  • Generazione QRT manuale: la compilazione manuale dei template Excel EIOPA e error-prone e non scalabile; automatizza la generazione XBRL dal data warehouse

Conclusioni: Fine della Serie InsurTech Engineering

La compliance engineering per Solvency II e IFRS 17 e uno dei domini più complessi e strategici dell'IT assicurativo. La sfida non e solo tecnica (calcoli attuariali, formato XBRL, timing dei calcoli) ma anche organizzativa: coordinare team attuariali, contabili, IT e compliance su un processo critico e regolamentato.

La buona notizia e che le tecnologie moderne - data warehouse columnar, pipeline ELT dichiarative (dbt), calcolo distribuito (Spark), automazione dei workflow - rendono finalmente possibile costruire sistemi di compliance scalabili, auditabili e aggiornabili rapidamente quando le normative evolvono.

Con questo articolo si conclude la serie InsurTech Engineering. Abbiamo percorso l'intero stack tecnologico del settore assicurativo moderno: dal dominio e il data model, alla policy management cloud-native, dalla telematica UBI, all'AI underwriting, all'automazione sinistri, alla fraud detection, agli standard ACORD, fino alla compliance regolamentare con Solvency II e IFRS 17.

Serie InsurTech Engineering - Articoli Completi

  • 01 - Dominio Assicurativo per Developer: Prodotti, Attori e Data Model
  • 02 - Policy Management Cloud-Native: Architettura API-First
  • 03 - Pipeline Telematica: Processing Dati UBI su Scala
  • 04 - AI Underwriting: Feature Engineering e Risk Scoring
  • 05 - Automazione Sinistri: Computer Vision e NLP
  • 06 - Fraud Detection: Graph Analytics e Behavioral Signal
  • 07 - Standard ACORD e API Integration Assicurative
  • 08 - Compliance Engineering: Solvency II e IFRS 17 (questo articolo)

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti