creditriskengine 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- creditriskengine/__init__.py +42 -0
- creditriskengine/core/__init__.py +46 -0
- creditriskengine/core/audit.py +191 -0
- creditriskengine/core/config.py +31 -0
- creditriskengine/core/exceptions.py +29 -0
- creditriskengine/core/exposure.py +117 -0
- creditriskengine/core/logging_config.py +88 -0
- creditriskengine/core/portfolio.py +48 -0
- creditriskengine/core/types.py +151 -0
- creditriskengine/ecl/__init__.py +17 -0
- creditriskengine/ecl/cecl/__init__.py +24 -0
- creditriskengine/ecl/cecl/cecl_calc.py +85 -0
- creditriskengine/ecl/cecl/methods.py +104 -0
- creditriskengine/ecl/cecl/qualitative.py +70 -0
- creditriskengine/ecl/ifrs9/__init__.py +30 -0
- creditriskengine/ecl/ifrs9/ecl_calc.py +142 -0
- creditriskengine/ecl/ifrs9/forward_looking.py +61 -0
- creditriskengine/ecl/ifrs9/lifetime_pd.py +110 -0
- creditriskengine/ecl/ifrs9/scenarios.py +70 -0
- creditriskengine/ecl/ifrs9/sicr.py +73 -0
- creditriskengine/ecl/ifrs9/staging.py +78 -0
- creditriskengine/ecl/ifrs9/ttc_to_pit.py +98 -0
- creditriskengine/ecl/ind_as109/__init__.py +14 -0
- creditriskengine/ecl/ind_as109/ind_as_ecl.py +97 -0
- creditriskengine/models/__init__.py +76 -0
- creditriskengine/models/concentration/__init__.py +17 -0
- creditriskengine/models/concentration/concentration.py +141 -0
- creditriskengine/models/concentration/granularity.py +12 -0
- creditriskengine/models/concentration/hhi.py +11 -0
- creditriskengine/models/concentration/sector.py +11 -0
- creditriskengine/models/ead/__init__.py +21 -0
- creditriskengine/models/ead/ccf.py +9 -0
- creditriskengine/models/ead/ead_estimation.py +9 -0
- creditriskengine/models/ead/ead_model.py +195 -0
- creditriskengine/models/ead/regulatory_ccf.py +9 -0
- creditriskengine/models/lgd/__init__.py +29 -0
- creditriskengine/models/lgd/cure_rate.py +261 -0
- creditriskengine/models/lgd/downturn_lgd.py +9 -0
- creditriskengine/models/lgd/lgd_model.py +210 -0
- creditriskengine/models/lgd/regulatory_lgd.py +9 -0
- creditriskengine/models/lgd/workout_lgd.py +9 -0
- creditriskengine/models/pd/__init__.py +81 -0
- creditriskengine/models/pd/binning.py +474 -0
- creditriskengine/models/pd/calibration.py +12 -0
- creditriskengine/models/pd/logistic.py +9 -0
- creditriskengine/models/pd/margin_of_conservatism.py +270 -0
- creditriskengine/models/pd/rating_scale.py +13 -0
- creditriskengine/models/pd/scorecard.py +346 -0
- creditriskengine/models/pd/structural.py +213 -0
- creditriskengine/models/pd/term_structure.py +162 -0
- creditriskengine/models/pd/transition_matrix.py +183 -0
- creditriskengine/models/pd/ttc_calibration.py +10 -0
- creditriskengine/models/pd/zscore.py +262 -0
- creditriskengine/portfolio/__init__.py +43 -0
- creditriskengine/portfolio/copula.py +194 -0
- creditriskengine/portfolio/economic_capital.py +55 -0
- creditriskengine/portfolio/stress_testing.py +1323 -0
- creditriskengine/portfolio/var.py +369 -0
- creditriskengine/portfolio/vasicek.py +198 -0
- creditriskengine/py.typed +0 -0
- creditriskengine/regulatory/__init__.py +11 -0
- creditriskengine/regulatory/australia/apra.yml +137 -0
- creditriskengine/regulatory/bcbs/bcbs_d424.yml +111 -0
- creditriskengine/regulatory/brazil/bcb.yml +143 -0
- creditriskengine/regulatory/canada/osfi.yml +139 -0
- creditriskengine/regulatory/china/nfra.yml +142 -0
- creditriskengine/regulatory/eu/crr3.yml +188 -0
- creditriskengine/regulatory/hongkong/hkma.yml +121 -0
- creditriskengine/regulatory/india/rbi.yml +123 -0
- creditriskengine/regulatory/japan/jfsa.yml +131 -0
- creditriskengine/regulatory/loader.py +72 -0
- creditriskengine/regulatory/malaysia/bnm.yml +147 -0
- creditriskengine/regulatory/saudi/sama.yml +114 -0
- creditriskengine/regulatory/schema.py +352 -0
- creditriskengine/regulatory/singapore/mas_637.yml +114 -0
- creditriskengine/regulatory/southafrica/sarb.yml +132 -0
- creditriskengine/regulatory/southkorea/fss.yml +134 -0
- creditriskengine/regulatory/uae/cbuae.yml +113 -0
- creditriskengine/regulatory/uk/pra_basel31.yml +143 -0
- creditriskengine/regulatory/us/us_endgame.yml +108 -0
- creditriskengine/reporting/__init__.py +27 -0
- creditriskengine/reporting/corep.py +453 -0
- creditriskengine/reporting/fr_y14.py +485 -0
- creditriskengine/reporting/model_doc.py +627 -0
- creditriskengine/reporting/pillar3.py +285 -0
- creditriskengine/reporting/reports.py +147 -0
- creditriskengine/reporting/templates/.gitkeep +0 -0
- creditriskengine/reporting/templates/model_doc.html.j2 +298 -0
- creditriskengine/rwa/__init__.py +15 -0
- creditriskengine/rwa/base.py +95 -0
- creditriskengine/rwa/capital_buffers.py +519 -0
- creditriskengine/rwa/crm.py +411 -0
- creditriskengine/rwa/cva.py +403 -0
- creditriskengine/rwa/irb/__init__.py +21 -0
- creditriskengine/rwa/irb/advanced.py +358 -0
- creditriskengine/rwa/irb/asset_classes.py +207 -0
- creditriskengine/rwa/irb/correlation.py +98 -0
- creditriskengine/rwa/irb/formulas.py +425 -0
- creditriskengine/rwa/irb/foundation.py +336 -0
- creditriskengine/rwa/irb/maturity.py +88 -0
- creditriskengine/rwa/irb/slotting.py +208 -0
- creditriskengine/rwa/leverage_ratio.py +256 -0
- creditriskengine/rwa/market_risk.py +294 -0
- creditriskengine/rwa/operational_risk.py +229 -0
- creditriskengine/rwa/output_floor.py +216 -0
- creditriskengine/rwa/securitisation.py +576 -0
- creditriskengine/rwa/standardized/__init__.py +25 -0
- creditriskengine/rwa/standardized/cre.py +194 -0
- creditriskengine/rwa/standardized/credit_risk_sa.py +661 -0
- creditriskengine/rwa/standardized/risk_weights.py +267 -0
- creditriskengine/validation/__init__.py +32 -0
- creditriskengine/validation/backtesting.py +338 -0
- creditriskengine/validation/benchmarking.py +316 -0
- creditriskengine/validation/calibration.py +233 -0
- creditriskengine/validation/discrimination.py +244 -0
- creditriskengine/validation/reporting.py +427 -0
- creditriskengine/validation/stability.py +123 -0
- creditriskengine-0.2.0.dist-info/METADATA +274 -0
- creditriskengine-0.2.0.dist-info/RECORD +121 -0
- creditriskengine-0.2.0.dist-info/WHEEL +4 -0
- creditriskengine-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CreditRiskEngine — Production-grade credit risk analytics.
|
|
3
|
+
|
|
4
|
+
Modules:
|
|
5
|
+
core: Data models and type definitions
|
|
6
|
+
rwa: Risk-weighted asset calculation (SA and IRB)
|
|
7
|
+
ecl: Expected credit loss engines (IFRS 9, CECL)
|
|
8
|
+
models: PD, LGD, EAD modeling pipelines
|
|
9
|
+
validation: Model validation toolkit
|
|
10
|
+
portfolio: Portfolio credit risk models
|
|
11
|
+
reporting: Regulatory reporting
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__version__ = "0.2.0"
|
|
15
|
+
|
|
16
|
+
# Public API — convenient top-level imports
|
|
17
|
+
from creditriskengine.core.exposure import Collateral, Exposure
|
|
18
|
+
from creditriskengine.core.portfolio import Portfolio
|
|
19
|
+
from creditriskengine.core.types import (
|
|
20
|
+
CollateralType,
|
|
21
|
+
CreditQualityStep,
|
|
22
|
+
CreditRiskApproach,
|
|
23
|
+
IFRS9Stage,
|
|
24
|
+
IRBAssetClass,
|
|
25
|
+
IRBRetailSubClass,
|
|
26
|
+
Jurisdiction,
|
|
27
|
+
SAExposureClass,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Collateral",
|
|
32
|
+
"CollateralType",
|
|
33
|
+
"CreditQualityStep",
|
|
34
|
+
"CreditRiskApproach",
|
|
35
|
+
"Exposure",
|
|
36
|
+
"IFRS9Stage",
|
|
37
|
+
"IRBAssetClass",
|
|
38
|
+
"IRBRetailSubClass",
|
|
39
|
+
"Jurisdiction",
|
|
40
|
+
"Portfolio",
|
|
41
|
+
"SAExposureClass",
|
|
42
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Core types, data models, and configuration for creditriskengine.
|
|
2
|
+
|
|
3
|
+
Provides regulatory type enumerations, exposure/portfolio data models,
|
|
4
|
+
configuration loading, and the exception hierarchy.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from creditriskengine.core.exceptions import (
|
|
8
|
+
CalculationError,
|
|
9
|
+
ConfigurationError,
|
|
10
|
+
CreditRiskEngineError,
|
|
11
|
+
DataError,
|
|
12
|
+
RegulatoryError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
from creditriskengine.core.exposure import Collateral, Exposure
|
|
16
|
+
from creditriskengine.core.portfolio import Portfolio
|
|
17
|
+
from creditriskengine.core.types import (
|
|
18
|
+
CollateralType,
|
|
19
|
+
CreditQualityStep,
|
|
20
|
+
CreditRiskApproach,
|
|
21
|
+
IFRS9Stage,
|
|
22
|
+
IRBAssetClass,
|
|
23
|
+
IRBRetailSubClass,
|
|
24
|
+
Jurisdiction,
|
|
25
|
+
SAExposureClass,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"CreditRiskEngineError",
|
|
30
|
+
"ConfigurationError",
|
|
31
|
+
"ValidationError",
|
|
32
|
+
"RegulatoryError",
|
|
33
|
+
"CalculationError",
|
|
34
|
+
"DataError",
|
|
35
|
+
"Jurisdiction",
|
|
36
|
+
"CreditRiskApproach",
|
|
37
|
+
"IRBAssetClass",
|
|
38
|
+
"IRBRetailSubClass",
|
|
39
|
+
"SAExposureClass",
|
|
40
|
+
"CreditQualityStep",
|
|
41
|
+
"IFRS9Stage",
|
|
42
|
+
"CollateralType",
|
|
43
|
+
"Exposure",
|
|
44
|
+
"Collateral",
|
|
45
|
+
"Portfolio",
|
|
46
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Audit trail and lineage tracking for capital calculations.
|
|
2
|
+
|
|
3
|
+
Provides immutable calculation records and an audit trail for governance
|
|
4
|
+
and regulatory compliance. Every RWA or ECL calculation can be recorded
|
|
5
|
+
with its full input/output context so that supervisory reviews, model
|
|
6
|
+
validation teams, and internal audit can trace any number back to its
|
|
7
|
+
regulatory basis.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from collections import Counter
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
import creditriskengine
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class CalculationRecord:
|
|
25
|
+
"""Immutable record of a single capital calculation."""
|
|
26
|
+
|
|
27
|
+
exposure_id: str
|
|
28
|
+
timestamp: datetime
|
|
29
|
+
approach: str
|
|
30
|
+
jurisdiction: str
|
|
31
|
+
inputs: dict[str, Any]
|
|
32
|
+
outputs: dict[str, Any]
|
|
33
|
+
regulatory_reference: str
|
|
34
|
+
engine_version: str
|
|
35
|
+
warnings: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AuditTrail:
|
|
39
|
+
"""Stores and queries :class:`CalculationRecord` entries.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
records : list[CalculationRecord] | None
|
|
44
|
+
Optional pre-existing records to initialise the trail with.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, records: list[CalculationRecord] | None = None) -> None:
|
|
48
|
+
self._records: list[CalculationRecord] = list(records) if records else []
|
|
49
|
+
|
|
50
|
+
# -- mutators -------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def record(
|
|
53
|
+
self,
|
|
54
|
+
exposure_id: str,
|
|
55
|
+
approach: str,
|
|
56
|
+
jurisdiction: str,
|
|
57
|
+
inputs: dict[str, Any],
|
|
58
|
+
outputs: dict[str, Any],
|
|
59
|
+
regulatory_reference: str,
|
|
60
|
+
warnings: list[str] | None = None,
|
|
61
|
+
) -> CalculationRecord:
|
|
62
|
+
"""Create and store a new :class:`CalculationRecord`.
|
|
63
|
+
|
|
64
|
+
``engine_version`` and ``timestamp`` are captured automatically.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
CalculationRecord
|
|
69
|
+
The newly created record.
|
|
70
|
+
"""
|
|
71
|
+
rec = CalculationRecord(
|
|
72
|
+
exposure_id=exposure_id,
|
|
73
|
+
timestamp=datetime.now(UTC),
|
|
74
|
+
approach=approach,
|
|
75
|
+
jurisdiction=jurisdiction,
|
|
76
|
+
inputs=dict(inputs),
|
|
77
|
+
outputs=dict(outputs),
|
|
78
|
+
regulatory_reference=regulatory_reference,
|
|
79
|
+
engine_version=creditriskengine.__version__,
|
|
80
|
+
warnings=list(warnings) if warnings else [],
|
|
81
|
+
)
|
|
82
|
+
self._records.append(rec)
|
|
83
|
+
return rec
|
|
84
|
+
|
|
85
|
+
# -- queries --------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def records(self) -> list[CalculationRecord]:
|
|
89
|
+
"""Return a shallow copy of the internal record list."""
|
|
90
|
+
return list(self._records)
|
|
91
|
+
|
|
92
|
+
def get_records(
|
|
93
|
+
self,
|
|
94
|
+
exposure_id: str | None = None,
|
|
95
|
+
approach: str | None = None,
|
|
96
|
+
) -> list[CalculationRecord]:
|
|
97
|
+
"""Return records matching the given filters.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
exposure_id : str | None
|
|
102
|
+
If provided, only records for this exposure are returned.
|
|
103
|
+
approach : str | None
|
|
104
|
+
If provided, only records using this approach are returned.
|
|
105
|
+
"""
|
|
106
|
+
result = self._records
|
|
107
|
+
if exposure_id is not None:
|
|
108
|
+
result = [r for r in result if r.exposure_id == exposure_id]
|
|
109
|
+
if approach is not None:
|
|
110
|
+
result = [r for r in result if r.approach == approach]
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
# -- export / reporting ---------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def to_dataframe(self) -> pd.DataFrame:
|
|
116
|
+
"""Convert the audit trail to a :class:`pandas.DataFrame`."""
|
|
117
|
+
if not self._records:
|
|
118
|
+
return pd.DataFrame(
|
|
119
|
+
columns=[
|
|
120
|
+
"exposure_id",
|
|
121
|
+
"timestamp",
|
|
122
|
+
"approach",
|
|
123
|
+
"jurisdiction",
|
|
124
|
+
"inputs",
|
|
125
|
+
"outputs",
|
|
126
|
+
"regulatory_reference",
|
|
127
|
+
"engine_version",
|
|
128
|
+
"warnings",
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
rows = []
|
|
132
|
+
for r in self._records:
|
|
133
|
+
rows.append(
|
|
134
|
+
{
|
|
135
|
+
"exposure_id": r.exposure_id,
|
|
136
|
+
"timestamp": r.timestamp,
|
|
137
|
+
"approach": r.approach,
|
|
138
|
+
"jurisdiction": r.jurisdiction,
|
|
139
|
+
"inputs": r.inputs,
|
|
140
|
+
"outputs": r.outputs,
|
|
141
|
+
"regulatory_reference": r.regulatory_reference,
|
|
142
|
+
"engine_version": r.engine_version,
|
|
143
|
+
"warnings": r.warnings,
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
return pd.DataFrame(rows)
|
|
147
|
+
|
|
148
|
+
def export_json(self, filepath: str) -> None:
|
|
149
|
+
"""Write the audit trail to a JSON file.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
filepath : str
|
|
154
|
+
Destination path (will be overwritten if it exists).
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
data = []
|
|
158
|
+
for r in self._records:
|
|
159
|
+
data.append(
|
|
160
|
+
{
|
|
161
|
+
"exposure_id": r.exposure_id,
|
|
162
|
+
"timestamp": r.timestamp.isoformat(),
|
|
163
|
+
"approach": r.approach,
|
|
164
|
+
"jurisdiction": r.jurisdiction,
|
|
165
|
+
"inputs": r.inputs,
|
|
166
|
+
"outputs": r.outputs,
|
|
167
|
+
"regulatory_reference": r.regulatory_reference,
|
|
168
|
+
"engine_version": r.engine_version,
|
|
169
|
+
"warnings": r.warnings,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
with open(filepath, "w") as fh:
|
|
173
|
+
json.dump(data, fh, indent=2)
|
|
174
|
+
|
|
175
|
+
def summary(self) -> dict[str, dict[str, int]]:
|
|
176
|
+
"""Aggregate statistics for the audit trail.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
dict
|
|
181
|
+
``{"by_approach": {<approach>: <count>}, "by_jurisdiction": {<jurisdiction>: <count>}}``
|
|
182
|
+
"""
|
|
183
|
+
by_approach: Counter[str] = Counter()
|
|
184
|
+
by_jurisdiction: Counter[str] = Counter()
|
|
185
|
+
for r in self._records:
|
|
186
|
+
by_approach[r.approach] += 1
|
|
187
|
+
by_jurisdiction[r.jurisdiction] += 1
|
|
188
|
+
return {
|
|
189
|
+
"by_approach": dict(by_approach),
|
|
190
|
+
"by_jurisdiction": dict(by_jurisdiction),
|
|
191
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Configuration loader for creditriskengine.
|
|
2
|
+
|
|
3
|
+
This module delegates to ``creditriskengine.regulatory.loader`` which is
|
|
4
|
+
the canonical implementation. It is kept for backward-compatibility so
|
|
5
|
+
that ``from creditriskengine.core.config import load_jurisdiction_config``
|
|
6
|
+
continues to work.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from creditriskengine.core.types import Jurisdiction
|
|
13
|
+
from creditriskengine.regulatory.loader import load_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_jurisdiction_config(
|
|
17
|
+
jurisdiction: Jurisdiction,
|
|
18
|
+
config_dir: Path | None = None,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"""Load regulatory configuration YAML for a jurisdiction.
|
|
21
|
+
|
|
22
|
+
Delegates to :func:`creditriskengine.regulatory.loader.load_config`.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
jurisdiction: Target jurisdiction.
|
|
26
|
+
config_dir: Override path to regulatory config directory.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Parsed YAML configuration dict.
|
|
30
|
+
"""
|
|
31
|
+
return load_config(jurisdiction, config_dir=config_dir)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Custom exception hierarchy for creditriskengine."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CreditRiskEngineError(Exception):
|
|
5
|
+
"""Base exception for all creditriskengine errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConfigurationError(CreditRiskEngineError):
|
|
9
|
+
"""Raised when configuration is invalid or missing."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class JurisdictionError(CreditRiskEngineError):
|
|
13
|
+
"""Raised when jurisdiction-specific config is not found."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidationError(CreditRiskEngineError):
|
|
17
|
+
"""Raised when input validation fails."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RegulatoryError(CreditRiskEngineError):
|
|
21
|
+
"""Raised when regulatory constraints are violated."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CalculationError(CreditRiskEngineError):
|
|
25
|
+
"""Raised when a calculation fails or produces invalid results."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DataError(CreditRiskEngineError):
|
|
29
|
+
"""Raised when input data is malformed or insufficient."""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exposure and facility data models.
|
|
3
|
+
|
|
4
|
+
Data models represent the minimum set of attributes required for
|
|
5
|
+
regulatory capital calculation under both SA and IRB approaches.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import date
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
|
11
|
+
|
|
12
|
+
from creditriskengine.core.types import (
|
|
13
|
+
CollateralType,
|
|
14
|
+
CreditQualityStep,
|
|
15
|
+
CreditRiskApproach,
|
|
16
|
+
IFRS9Stage,
|
|
17
|
+
IRBAssetClass,
|
|
18
|
+
IRBCorporateSubClass,
|
|
19
|
+
IRBRetailSubClass,
|
|
20
|
+
Jurisdiction,
|
|
21
|
+
SAExposureClass,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Collateral(BaseModel):
|
|
26
|
+
"""Collateral pledged against an exposure."""
|
|
27
|
+
collateral_type: CollateralType
|
|
28
|
+
value: float = Field(ge=0, description="Current market/appraised value")
|
|
29
|
+
currency: str = Field(default="USD", max_length=3)
|
|
30
|
+
haircut: float | None = Field(
|
|
31
|
+
default=None, ge=0, le=1, description="Supervisory or own-estimate haircut"
|
|
32
|
+
)
|
|
33
|
+
ltv: float | None = Field(default=None, ge=0, description="Loan-to-value ratio at origination")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Exposure(BaseModel):
|
|
37
|
+
"""
|
|
38
|
+
Single credit exposure / facility for capital calculation.
|
|
39
|
+
|
|
40
|
+
Contains all fields needed for SA, F-IRB, and A-IRB RWA
|
|
41
|
+
computation, IFRS 9/CECL ECL calculation, and model validation.
|
|
42
|
+
"""
|
|
43
|
+
# ---- Identifiers ----
|
|
44
|
+
exposure_id: str
|
|
45
|
+
counterparty_id: str
|
|
46
|
+
|
|
47
|
+
# ---- Amounts ----
|
|
48
|
+
ead: float = Field(ge=0, description="Exposure at Default amount")
|
|
49
|
+
drawn_amount: float = Field(ge=0, description="Current drawn/outstanding balance")
|
|
50
|
+
undrawn_commitment: float = Field(default=0, ge=0, description="Undrawn committed amount")
|
|
51
|
+
|
|
52
|
+
# ---- Classification ----
|
|
53
|
+
jurisdiction: Jurisdiction
|
|
54
|
+
approach: CreditRiskApproach
|
|
55
|
+
sa_exposure_class: SAExposureClass | None = None
|
|
56
|
+
irb_asset_class: IRBAssetClass | None = None
|
|
57
|
+
irb_corporate_subclass: IRBCorporateSubClass | None = None
|
|
58
|
+
irb_retail_subclass: IRBRetailSubClass | None = None
|
|
59
|
+
|
|
60
|
+
# ---- SA Parameters ----
|
|
61
|
+
credit_quality_step: CreditQualityStep | None = None
|
|
62
|
+
is_investment_grade: bool | None = None
|
|
63
|
+
|
|
64
|
+
# ---- IRB Parameters ----
|
|
65
|
+
pd: float | None = Field(default=None, ge=0, le=1, description="Probability of Default")
|
|
66
|
+
lgd: float | None = Field(default=None, ge=0, le=1, description="Loss Given Default")
|
|
67
|
+
ead_model: float | None = Field(default=None, ge=0, description="EAD from internal model")
|
|
68
|
+
maturity_years: float | None = Field(
|
|
69
|
+
default=None, ge=0, le=30, description="Effective residual maturity M in years"
|
|
70
|
+
)
|
|
71
|
+
turnover_eur_millions: float | None = Field(
|
|
72
|
+
default=None, ge=0, description="Annual turnover for SME firm-size adjustment"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# ---- Collateral and CRM ----
|
|
76
|
+
collaterals: list[Collateral] = Field(default_factory=list)
|
|
77
|
+
is_guaranteed: bool = False
|
|
78
|
+
guarantor_risk_weight: float | None = None
|
|
79
|
+
|
|
80
|
+
# ---- Real Estate ----
|
|
81
|
+
property_value: float | None = Field(default=None, ge=0)
|
|
82
|
+
ltv_ratio: float | None = Field(default=None, ge=0)
|
|
83
|
+
is_income_producing: bool = False
|
|
84
|
+
is_materially_dependent_on_cashflows: bool = False
|
|
85
|
+
|
|
86
|
+
# ---- IFRS 9 / ECL ----
|
|
87
|
+
ifrs9_stage: IFRS9Stage | None = None
|
|
88
|
+
days_past_due: int = Field(default=0, ge=0)
|
|
89
|
+
origination_date: date | None = None
|
|
90
|
+
maturity_date: date | None = None
|
|
91
|
+
origination_pd: float | None = Field(
|
|
92
|
+
default=None, ge=0, le=1, description="12-month PD at origination"
|
|
93
|
+
)
|
|
94
|
+
current_pd: float | None = Field(default=None, ge=0, le=1, description="Current 12-month PD")
|
|
95
|
+
is_credit_impaired: bool = False
|
|
96
|
+
effective_interest_rate: float | None = Field(
|
|
97
|
+
default=None, ge=0, description="EIR for ECL discounting"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# ---- Flags ----
|
|
101
|
+
is_defaulted: bool = False
|
|
102
|
+
is_in_default_workout: bool = False
|
|
103
|
+
currency: str = Field(default="USD", max_length=3)
|
|
104
|
+
|
|
105
|
+
@field_validator("pd")
|
|
106
|
+
@classmethod
|
|
107
|
+
def pd_floor_check(cls, v: float | None) -> float | None:
|
|
108
|
+
"""Validate PD is between 0 and 1 (inclusive).
|
|
109
|
+
|
|
110
|
+
Note: The Basel III PD floor of 0.03% (CRE32.13) is enforced at the
|
|
111
|
+
IRB calculation level (``rwa.irb.formulas``), not at the data model
|
|
112
|
+
level, because SA and ECL workflows accept PDs below the IRB floor.
|
|
113
|
+
"""
|
|
114
|
+
if v is not None and not 0 <= v <= 1:
|
|
115
|
+
msg = f"PD must be between 0 and 1, got {v}"
|
|
116
|
+
raise ValueError(msg)
|
|
117
|
+
return v
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured logging configuration for CreditRiskEngine.
|
|
3
|
+
|
|
4
|
+
Provides sensible defaults for production and development use.
|
|
5
|
+
Supports JSON structured logging for log aggregation systems
|
|
6
|
+
(ELK, Splunk, CloudWatch, etc.).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from creditriskengine.core.logging_config import configure_logging
|
|
10
|
+
configure_logging() # Development defaults
|
|
11
|
+
configure_logging(level="WARNING", json_format=True) # Production
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import UTC, datetime
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JSONFormatter(logging.Formatter):
|
|
22
|
+
"""Structured JSON log formatter for production environments.
|
|
23
|
+
|
|
24
|
+
Produces one JSON object per log line, suitable for ingestion by
|
|
25
|
+
log aggregation tools (ELK stack, Splunk, AWS CloudWatch, etc.).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
29
|
+
log_entry: dict[str, Any] = {
|
|
30
|
+
"timestamp": datetime.fromtimestamp(
|
|
31
|
+
record.created, tz=UTC
|
|
32
|
+
).isoformat(),
|
|
33
|
+
"level": record.levelname,
|
|
34
|
+
"logger": record.name,
|
|
35
|
+
"message": record.getMessage(),
|
|
36
|
+
"module": record.module,
|
|
37
|
+
"function": record.funcName,
|
|
38
|
+
"line": record.lineno,
|
|
39
|
+
}
|
|
40
|
+
if record.exc_info and record.exc_info[1]:
|
|
41
|
+
log_entry["exception"] = self.formatException(record.exc_info)
|
|
42
|
+
# Include any extra fields attached to the record
|
|
43
|
+
for key in ("exposure_id", "jurisdiction", "approach", "calculation_id"):
|
|
44
|
+
val = getattr(record, key, None)
|
|
45
|
+
if val is not None:
|
|
46
|
+
log_entry[key] = val
|
|
47
|
+
return json.dumps(log_entry, default=str)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def configure_logging(
|
|
51
|
+
level: str = "INFO",
|
|
52
|
+
json_format: bool = False,
|
|
53
|
+
log_file: str | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Configure logging for CreditRiskEngine.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
|
59
|
+
json_format: If True, use JSON structured format (recommended for production).
|
|
60
|
+
log_file: Optional file path for log output. If None, logs to stderr.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> from creditriskengine.core.logging_config import configure_logging
|
|
64
|
+
>>> configure_logging(level="DEBUG") # Development
|
|
65
|
+
>>> configure_logging(level="WARNING", json_format=True) # Production
|
|
66
|
+
>>> configure_logging(level="INFO", log_file="/var/log/cre.log")
|
|
67
|
+
"""
|
|
68
|
+
root_logger = logging.getLogger("creditriskengine")
|
|
69
|
+
root_logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
|
70
|
+
|
|
71
|
+
# Remove existing handlers to avoid duplicates
|
|
72
|
+
root_logger.handlers.clear()
|
|
73
|
+
|
|
74
|
+
if json_format:
|
|
75
|
+
formatter = JSONFormatter()
|
|
76
|
+
else:
|
|
77
|
+
formatter = logging.Formatter(
|
|
78
|
+
fmt="%(asctime)s [%(levelname)-8s] %(name)s: %(message)s",
|
|
79
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if log_file:
|
|
83
|
+
handler: logging.Handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
84
|
+
else:
|
|
85
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
86
|
+
|
|
87
|
+
handler.setFormatter(formatter)
|
|
88
|
+
root_logger.addHandler(handler)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Portfolio container for credit exposures."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from creditriskengine.core.exposure import Exposure
|
|
8
|
+
from creditriskengine.core.types import CreditRiskApproach, Jurisdiction
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Portfolio(BaseModel):
|
|
12
|
+
"""Container for a portfolio of credit exposures.
|
|
13
|
+
|
|
14
|
+
Provides iteration, filtering, and aggregation over exposures.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
18
|
+
|
|
19
|
+
name: str = "Unnamed Portfolio"
|
|
20
|
+
jurisdiction: Jurisdiction | None = None
|
|
21
|
+
approach: CreditRiskApproach | None = None
|
|
22
|
+
exposures: list[Exposure] = Field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
def add_exposure(self, exposure: Exposure) -> None:
|
|
25
|
+
"""Add an exposure to the portfolio."""
|
|
26
|
+
self.exposures.append(exposure)
|
|
27
|
+
|
|
28
|
+
def total_ead(self) -> float:
|
|
29
|
+
"""Total Exposure at Default across all exposures."""
|
|
30
|
+
return sum(e.ead for e in self.exposures)
|
|
31
|
+
|
|
32
|
+
def __len__(self) -> int:
|
|
33
|
+
return len(self.exposures)
|
|
34
|
+
|
|
35
|
+
def __iter__(self) -> Iterator[Exposure]: # type: ignore[override]
|
|
36
|
+
return iter(self.exposures)
|
|
37
|
+
|
|
38
|
+
def filter_by_approach(self, approach: CreditRiskApproach) -> list[Exposure]:
|
|
39
|
+
"""Return exposures using a specific calculation approach."""
|
|
40
|
+
return [e for e in self.exposures if e.approach == approach]
|
|
41
|
+
|
|
42
|
+
def filter_defaulted(self) -> list[Exposure]:
|
|
43
|
+
"""Return defaulted exposures."""
|
|
44
|
+
return [e for e in self.exposures if e.is_defaulted]
|
|
45
|
+
|
|
46
|
+
def filter_non_defaulted(self) -> list[Exposure]:
|
|
47
|
+
"""Return non-defaulted exposures."""
|
|
48
|
+
return [e for e in self.exposures if not e.is_defaulted]
|