규정 준수 엔지니어링: 플랫폼 구축자를 위한 Solvency II 및 IFRS 17
규정 준수만큼 복잡하고 위험성이 높은 기술 영역은 거의 없습니다. 보험 규정. Solvency II와 IFRS 17은 두 가지 규제 프레임워크입니다. 지불 능력(보유할 자본의 양)과 재무 보고(어떻게 보유해야 하는지) 보험 계약 회계) 유럽 기업의 경우. 구현이 잘못됨 이러한 프레임워크의 기술은 제재, 운영 제한 및 평판 손실을 의미합니다.
개발자의 과제는 이러한 프레임워크가 기술로 설계되지 않았다는 것입니다. 명심하세요: 이는 소프트웨어 시스템으로 변환되어야 하는 보험계리 및 회계 규정입니다. 대부분의 회사는 Excel 시트 기반 및 배치 기반 구현에 어려움을 겪고 있습니다. 지난 몇 시간 동안의 밤. 좋은 소식은 최신 아키텍처인 데이터 웨어하우스가 컬럼형, ELT 파이프라인, 분산 컴퓨팅 — 마침내 시스템 구축이 가능해졌습니다. 규정 준수 확장 가능 e 감사 가능.
PwC에 따르면 Solvency II 및 IFRS 17 규정 준수는 다음과 같은 독특한 기회를 제공합니다. 규정은 많은 입력 데이터를 공유합니다. 대신 보고 파이프라인을 통합합니다. 별도로 보관하면 40-60% 연간 운영 비용 규정 준수.
무엇을 배울 것인가
- Solvency II 기술 개요: Pillar 1(SCR), Pillar 2(ORSA), Pillar 3(보고)
- IFRS 17 데이터 모델: 계약 그룹, 측정 모델(GMM, PAA, VFA)
- 보험 규정 준수를 위한 데이터 웨어하우스 아키텍처
- Python 및 dbt를 사용한 SCR 계산 파이프라인
- XBRL 형식의 Solvency II 보고서(QRT) 구성
- IFRS 17 데이터 모델 및 부채 측정 계산
- Solvency II + IFRS 17 통합: 데이터 공유 및 중복 감소
Solvency II: 개발자를 위한 기술 개요
Solvency II 및 유럽 보험 지급 능력 프레임워크(지침 2009/138/EC), 세 가지 기둥으로 구성됩니다.
- 원칙 1 - 정량적 요구 사항: 지급 능력 자본 요건(SCR), 최소 자본 요건(MCR) 및 기술 준비금(최적 추정치 + 위험 마진) 계산
- 원칙 2 - 거버넌스 및 위험 관리: ORSA(Own Risk and Solvency Assessment), 내부통제시스템, 핵심기능
- 원칙 3 - 보고 및 투명성: EIOPA용 QRT(정량적 보고 템플릿), 공개 SFCR(지불 능력 및 재무 상태 보고서), 감독자를 위한 RSR
플랫폼 빌더의 주요 작업 지점은 다음과 같습니다. 계산 파이프라인 기술 보유량 및 SCR(Pillar 1), QRT 보고를 위한 데이터 인프라(Pillar 3) Pillar 2에 대한 감사 추적 시스템.
Solvency II 데이터 구성 요소
| 요소 | 설명 | 주파수 계산 | 기술 출력 |
|---|---|---|---|
| BEL(최적 추정 책임) | 미래현금흐름의 기대현재가치 | 분기별/연간 | 연도/라인별 현금 흐름이 포함된 테이블 |
| 위험마진(RM) | 헤징할 수 없는 위험에 대한 자본 비용 | 분기별/연간 | 사업 분야별 스칼라 값 |
| SCR(표준 공식) | 16개 위험 모듈에 대한 충격에 필요한 자본 | 연간(YE), 반기마다 | 상관관계 + 집계 행렬 |
| QRT(정량적 보고 템플릿) | 규제 보고용 EIOPA 템플릿 | 분기별 + 연간 | XBRL, 엑셀 템플릿 EIOPA |
| ORSA 보고서 | 자체 위험 및 지급여력 평가 | 연간 | PDF 문서 + 지원 데이터 |
규정 준수를 위한 데이터 웨어하우스 아키텍처
보험 규정을 준수하려면 기록화라는 특정 특성을 지닌 데이터 웨어하우스가 필요합니다. 완전성(감사 추적), 각 변환의 추적성, 서로 다른 시스템 간의 조정, 데이터가 수정될 때 과거 기간을 재처리하는 능력(최신 수정 사항).
-- ============================================================
-- 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)
);
Python을 사용한 최상의 견적 계산 파이프라인
BEL(Best Estimate Liability) 계산은 Solvency II의 보험 통계 핵심입니다. 측면에서 기술적으로 예상되는 미래 현금 흐름(청구, 비용, 보험료)을 예측하고 이를 할인해야 합니다. 적절한 EIOPA 무위험 수익률 곡선을 사용합니다. 다음 코드는 버전을 구현합니다. 손해보험 지점에 대한 단순화된 BEL 계산.
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: 데이터 모델 및 구현
IFRS 17(EU 기업에 대해 2023년 1월 1일부터 시행)은 회계에 혁명을 일으켰습니다. 보험 계약. 핵심 원칙은 보험 계약이 더 이상 유효하지 않다는 것입니다. 보험료를 징수할 때 회계처리하지만 그 수익성은 인식합니다. 미래 기대치(계약 서비스 마진 - CSM) 및 청구는 비용으로 측정됨 현재(과거 비용이 아닌 현재 가치).
IFRS 17의 세 가지 주요 측정 모델은 다음과 같습니다.
- 일반 측정 모델(GMM): 메인모델; BEL 계산(할인) + 위험 조정 + CSM
- 프리미엄 할당 접근법(PAA): 이전 IFRS 4와 유사하게 단기 계약(최대 1년) 단순화
- 가변 수수료 접근법(VFA): 이익을 공유하는 종신 계약의 경우
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),
)
Solvency II에 대한 QRT XBRL 생성
QRT(정량적 보고 템플릿)는 표준화된 EIOPA 템플릿입니다. 회사는 감독 기관(이탈리아: IVASS)에 자주 전송해야 합니다. 분기별 및 매년. 2016년부터 필수 전송 형식은 XBRL(eXtensible)입니다. 비즈니스 보고 언어). 데이터 웨어하우스에서 QRT 자동 생성 e 규정 준수 엔지니어링의 가장 영향력 있는 사용 사례 중 하나입니다.
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)
Solvency II + IFRS 17 통합: 공유 데이터
규정 준수 비용을 절감할 수 있는 가장 좋은 기회는 Solvency II 및 IFRS 17이 사용하는 것입니다. 공통된 많은 데이터: 둘 다 부채에 대한 최선의 추정이 필요합니다(계산된 경우에도). 방법론이 다름) 둘 다 할인을 위한 수익률 곡선이 필요합니다. 다름: Solvency II에 대한 EIOPA 무위험, IFRS 17에 대한 현재 할인율), 둘 다 그들은 사업 분야별로 분류됩니다.
Solvency II / IFRS 17 공유 데이터
| 주어진 | 솔벤시 II | IFRS 17 | 주요 차이점 |
|---|---|---|---|
| 최선의 추정 책임 | BEL(무위험 할인) | FCF - BEL 구성요소 | 다양한 수익률 곡선, 다양한 현금흐름 정의 |
| 수익률 곡선 | EIOPA 무위험 + VA/MA | 현재 + 고정 금리 | IFRS 17의 두 개의 개별 곡선 |
| LoB 세분화 | EIOPA 라인 17개 | 더 많은 집계된 라인 | 매핑 필요 |
| 개발 삼각형 | 체인사다리용 | FCF의 경우 | 동일한 소스 데이터 |
| 정책/계약 데이터 | 적립금 계산을 위해 | 계약 그룹별 | 동일한 소스 데이터, 다른 집계 |
모범 사례 및 안티패턴
규정 준수 엔지니어링 모범 사례
- 입력 데이터에 대한 단일 진실 소스: 청구, 보험 증서 및 보험료는 인증된 단일 데이터 웨어하우스에서 나와야 합니다. Solvency II 및 IFRS 17 계산은 동일한 원시 데이터에서 시작해야 합니다.
- 각 실행에 대한 변경 불가능한 감사 추적: 각 계산 실행은 코드 버전, 입력 데이터, 매개변수, 출력(계리 및 규제 검토에 필요)이 포함된 완전한 로그를 생성해야 합니다.
- 데이터 분리와 계산: 데이터 웨어하우스에는 기록 및 선별된 데이터만 포함되어야 합니다. 계산 논리는 SQL 저장 프로시저가 아닌 애플리케이션 계층(Python/R)에 있어야 합니다.
- 필수 보험계리 테스트: 계산 모델에 대한 각 변경에는 이전 기간과의 비교 및 편차 분석을 통해 보험계리팀의 검증이 수반되어야 합니다.
- 마감 주기 자동화: QRT 계산, 검증 및 전송 프로세스는 SLA에 대한 시간 트리거 및 경고를 통해 완전히 자동화되어야 합니다.
피해야 할 안티패턴
- 생산 계산 시스템으로서의 Excel: Excel 시트는 감사할 수 없고 확장할 수 없으며 버전 제어를 지원하지 않습니다. 모든 규제 계산은 버전이 지정된 코드에 있어야 합니다.
- Solvency II 및 IFRS 17에 대한 별도 데이터: 두 개의 별도 파이프라인을 유지하면 비용과 위험이 중복됩니다. 중복되어 두 프레임워크 간에 비용이 많이 드는 조정이 발생합니다.
- 수익률 곡선을 하드코딩합니다. EIOPA 무위험 곡선은 매달 변경됩니다. 수동으로 입력하는 것이 아니라 EIOPA 웹사이트에서 자동으로 업로드해야 합니다.
- 수동 QRT 생성: EIOPA와 오류가 발생하기 쉽고 확장이 불가능한 Excel 템플릿을 수동으로 편집합니다. 데이터 웨어하우스에서 XBRL 생성을 자동화합니다.
결론: InsurTech 엔지니어링 시리즈의 끝
Solvency II 및 IFRS 17에 대한 규정 준수 엔지니어링은 가장 복잡한 영역 중 하나이며 보험 IT 전략. 문제는 기술적인 것(계리 계산, XBRL 형식, 계산 시기)뿐만 아니라 조직적: 계리, 회계, IT 및 중요하고 규제된 프로세스에 대한 규정 준수.
좋은 소식은 기둥형 데이터 웨어하우스, 선언적 ELT 파이프라인 등 현대 기술이 (dbt), 분산 컴퓨팅(Spark), 워크플로 자동화 — 마침내 가능해졌습니다. 확장 가능하고 감사 가능하며 신속하게 업데이트 가능한 규정 준수 시스템을 구축하세요. 규제가 진화합니다.
이 기사로 시리즈를 마무리합니다 인슈어테크엔지니어링. 우리는 도메인과 데이터부터 현대 보험 산업의 전체 기술 스택을 다루었습니다. 모델부터 클라우드 네이티브 정책 관리, UBI 텔레매틱스, AI 인수까지, 청구 자동화, 사기 탐지, ACORD 표준, 규정 준수까지 Solvency II 및 IFRS 17의 규제를 받습니다.
InsurTech 엔지니어링 시리즈 - 전체 기사
- 01 - 개발자를 위한 보험 도메인: 제품, 행위자 및 데이터 모델
- 02 - 클라우드 네이티브 정책 관리: API 우선 아키텍처
- 03 - 텔레매틱스 파이프라인: 대규모 UBI 데이터 처리
- 04 - AI Underwriting: 기능 엔지니어링 및 위험 평가
- 05 - 청구 자동화: 컴퓨터 비전 및 NLP
- 06 - 사기 탐지: 그래프 분석 및 행동 신호
- 07 - ACORD 표준 및 보험 API 통합
- 08 - 규정 준수 엔지니어링: Solvency II 및 IFRS 17(본 기사)







