updatesupport-finance 0.1.0__tar.gz
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.
- updatesupport_finance-0.1.0/LICENSE +21 -0
- updatesupport_finance-0.1.0/MANIFEST.in +3 -0
- updatesupport_finance-0.1.0/PKG-INFO +186 -0
- updatesupport_finance-0.1.0/README.md +162 -0
- updatesupport_finance-0.1.0/examples/model_risk_portfolio.py +326 -0
- updatesupport_finance-0.1.0/pyproject.toml +57 -0
- updatesupport_finance-0.1.0/setup.cfg +4 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance/__init__.py +42 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance/metrics.py +93 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance/plugin.py +37 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance/portfolio.py +258 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance/presets.py +21 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance.egg-info/PKG-INFO +186 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance.egg-info/SOURCES.txt +17 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance.egg-info/dependency_links.txt +1 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance.egg-info/entry_points.txt +2 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance.egg-info/requires.txt +1 -0
- updatesupport_finance-0.1.0/src/updatesupport_finance.egg-info/top_level.txt +1 -0
- updatesupport_finance-0.1.0/tests/test_finance.py +187 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 updatesupport contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: updatesupport-finance
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Financial model-risk extensions for updatesupport
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/nahuaque/updatesupport
|
|
7
|
+
Project-URL: Repository, https://github.com/nahuaque/updatesupport
|
|
8
|
+
Project-URL: Issues, https://github.com/nahuaque/updatesupport/issues
|
|
9
|
+
Keywords: credit-risk,expected-loss,financial-model-risk,model-validation,updatesupport
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: updatesupport>=0.1.1
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# updatesupport-finance
|
|
26
|
+
|
|
27
|
+
Financial model-risk extensions for
|
|
28
|
+
[`updatesupport`](https://pypi.org/project/updatesupport/).
|
|
29
|
+
|
|
30
|
+
`updatesupport-finance` audits whether a public risk segmentation is stable
|
|
31
|
+
enough to support a reported portfolio metric.
|
|
32
|
+
|
|
33
|
+
The core question is:
|
|
34
|
+
|
|
35
|
+
> If a model report only shows risk by coarse public buckets such as
|
|
36
|
+
> `product x region x FICO band x LTV band`, could the reported expected-loss
|
|
37
|
+
> estimate materially change if the hidden mix inside those buckets shifted?
|
|
38
|
+
|
|
39
|
+
This is a segmentation adequacy check for reported risk metrics. It is designed
|
|
40
|
+
for model-review and portfolio-monitoring artifacts, not as a replacement for
|
|
41
|
+
model validation, calibration, backtesting, or statistical uncertainty analysis.
|
|
42
|
+
|
|
43
|
+
Install directly:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install updatesupport-finance
|
|
47
|
+
uv add updatesupport-finance
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or through the core package extra:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "updatesupport[finance]"
|
|
54
|
+
uv add "updatesupport[finance]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The package provides finance-oriented row metrics, Q preset aliases, portfolio
|
|
58
|
+
compilation, and a model-risk report profile while keeping financial vocabulary
|
|
59
|
+
out of the core `updatesupport` package.
|
|
60
|
+
|
|
61
|
+
## Why This Is Useful
|
|
62
|
+
|
|
63
|
+
Financial analysts already monitor model performance, population drift,
|
|
64
|
+
calibration, overrides, and scenario sensitivity. Those checks usually ask
|
|
65
|
+
whether the model or portfolio changed.
|
|
66
|
+
|
|
67
|
+
`updatesupport-finance` asks a different question:
|
|
68
|
+
|
|
69
|
+
> Is the reporting segmentation itself adequate for the metric being reported?
|
|
70
|
+
|
|
71
|
+
For example, a validation pack may report expected loss by:
|
|
72
|
+
|
|
73
|
+
- `product`
|
|
74
|
+
- `region`
|
|
75
|
+
- `fico_band`
|
|
76
|
+
- `ltv_band`
|
|
77
|
+
|
|
78
|
+
But inside those public buckets, hidden composition may vary by:
|
|
79
|
+
|
|
80
|
+
- broker channel
|
|
81
|
+
- employment type
|
|
82
|
+
- vintage
|
|
83
|
+
- hardship history
|
|
84
|
+
- documentation type
|
|
85
|
+
- local housing market
|
|
86
|
+
- borrower cashflow pattern
|
|
87
|
+
|
|
88
|
+
If those hidden subgroups have different expected-loss rates, the public
|
|
89
|
+
segmentation may not fully support the reported aggregate. The report quantifies
|
|
90
|
+
that hidden-composition ambiguity and identifies which hidden variables would
|
|
91
|
+
most improve the public segmentation.
|
|
92
|
+
|
|
93
|
+
## What The Report Separates
|
|
94
|
+
|
|
95
|
+
The package is intentionally narrow. It separates:
|
|
96
|
+
|
|
97
|
+
- reported risk estimate: the supplied metric, such as expected loss or default
|
|
98
|
+
rate
|
|
99
|
+
- statistical uncertainty: confidence intervals or model uncertainty supplied by
|
|
100
|
+
other workflows
|
|
101
|
+
- hidden-composition ambiguity: how far the reported metric can move when hidden
|
|
102
|
+
mix shifts inside fixed public buckets
|
|
103
|
+
- refinement recommendations: hidden fields that would make the public
|
|
104
|
+
representation more stable
|
|
105
|
+
|
|
106
|
+
This is not a confidence interval and not a full model-risk-management system.
|
|
107
|
+
It is a reviewable control for one practical question: whether the reporting
|
|
108
|
+
representation is stable enough for the risk metric.
|
|
109
|
+
|
|
110
|
+
## Analyst Workflow
|
|
111
|
+
|
|
112
|
+
1. Choose public buckets from the model report.
|
|
113
|
+
2. Choose hidden refinements that are available internally but not shown in the
|
|
114
|
+
public segmentation.
|
|
115
|
+
3. Choose the target risk metric.
|
|
116
|
+
4. Choose a plausible hidden-mix shift preset.
|
|
117
|
+
5. Set a review threshold for hidden-composition ambiguity.
|
|
118
|
+
6. Attach the generated Markdown report to a model-review or monitoring pack.
|
|
119
|
+
|
|
120
|
+
The review status is deliberately simple:
|
|
121
|
+
|
|
122
|
+
- `pass`: ambiguity and public adequacy checks are within the chosen thresholds
|
|
123
|
+
- `attention required`: the public segmentation may need refinement or explicit
|
|
124
|
+
acceptance of the ambiguity band
|
|
125
|
+
|
|
126
|
+
## Example
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import updatesupport_finance as usf
|
|
130
|
+
|
|
131
|
+
report = usf.model_risk_report(
|
|
132
|
+
portfolio,
|
|
133
|
+
public=["product", "region", "fico_band", "ltv_band"],
|
|
134
|
+
hidden=[
|
|
135
|
+
"product",
|
|
136
|
+
"region",
|
|
137
|
+
"fico_band",
|
|
138
|
+
"ltv_band",
|
|
139
|
+
"broker_channel",
|
|
140
|
+
"employment_type",
|
|
141
|
+
"vintage",
|
|
142
|
+
],
|
|
143
|
+
metric=usf.expected_loss(pd="pd", lgd="lgd"),
|
|
144
|
+
exposure="ead",
|
|
145
|
+
q=usf.q_portfolio_mix_shift(radius=0.25),
|
|
146
|
+
model_id="EL_RETAIL_2026Q2",
|
|
147
|
+
portfolio_name="Retail credit portfolio",
|
|
148
|
+
as_of_date="2026-06-30",
|
|
149
|
+
intended_use="Expected-loss segmentation model review",
|
|
150
|
+
ambiguity_limit=0.0025,
|
|
151
|
+
public_adequacy_required=False,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
print(report.to_markdown())
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The report answers:
|
|
158
|
+
|
|
159
|
+
- What is the reported portfolio risk estimate?
|
|
160
|
+
- What range is still possible under hidden mix shifts?
|
|
161
|
+
- Does the ambiguity exceed the review threshold?
|
|
162
|
+
- Which public buckets drive the instability?
|
|
163
|
+
- Which hidden fields are most valuable as public refinements?
|
|
164
|
+
- Which small public segmentation sits on the stability frontier, and why did
|
|
165
|
+
it beat nearby alternatives?
|
|
166
|
+
|
|
167
|
+
A synthetic portfolio example is available in `examples/model_risk_portfolio.py`
|
|
168
|
+
in the source repository:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv run --package updatesupport-finance python \
|
|
172
|
+
packages/updatesupport-finance/examples/model_risk_portfolio.py
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The example prints both the finance model-risk report and a core
|
|
176
|
+
`public_representation_frontier(...)` report for the same expected-loss metric.
|
|
177
|
+
The frontier section compares baseline versus selected ambiguity, close
|
|
178
|
+
dominated alternatives, and any screened-out refinement fields.
|
|
179
|
+
|
|
180
|
+
To write the Markdown report:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
uv run --package updatesupport-finance python \
|
|
184
|
+
packages/updatesupport-finance/examples/model_risk_portfolio.py \
|
|
185
|
+
--output data/finance_model_risk_report.md
|
|
186
|
+
```
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# updatesupport-finance
|
|
2
|
+
|
|
3
|
+
Financial model-risk extensions for
|
|
4
|
+
[`updatesupport`](https://pypi.org/project/updatesupport/).
|
|
5
|
+
|
|
6
|
+
`updatesupport-finance` audits whether a public risk segmentation is stable
|
|
7
|
+
enough to support a reported portfolio metric.
|
|
8
|
+
|
|
9
|
+
The core question is:
|
|
10
|
+
|
|
11
|
+
> If a model report only shows risk by coarse public buckets such as
|
|
12
|
+
> `product x region x FICO band x LTV band`, could the reported expected-loss
|
|
13
|
+
> estimate materially change if the hidden mix inside those buckets shifted?
|
|
14
|
+
|
|
15
|
+
This is a segmentation adequacy check for reported risk metrics. It is designed
|
|
16
|
+
for model-review and portfolio-monitoring artifacts, not as a replacement for
|
|
17
|
+
model validation, calibration, backtesting, or statistical uncertainty analysis.
|
|
18
|
+
|
|
19
|
+
Install directly:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install updatesupport-finance
|
|
23
|
+
uv add updatesupport-finance
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or through the core package extra:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install "updatesupport[finance]"
|
|
30
|
+
uv add "updatesupport[finance]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package provides finance-oriented row metrics, Q preset aliases, portfolio
|
|
34
|
+
compilation, and a model-risk report profile while keeping financial vocabulary
|
|
35
|
+
out of the core `updatesupport` package.
|
|
36
|
+
|
|
37
|
+
## Why This Is Useful
|
|
38
|
+
|
|
39
|
+
Financial analysts already monitor model performance, population drift,
|
|
40
|
+
calibration, overrides, and scenario sensitivity. Those checks usually ask
|
|
41
|
+
whether the model or portfolio changed.
|
|
42
|
+
|
|
43
|
+
`updatesupport-finance` asks a different question:
|
|
44
|
+
|
|
45
|
+
> Is the reporting segmentation itself adequate for the metric being reported?
|
|
46
|
+
|
|
47
|
+
For example, a validation pack may report expected loss by:
|
|
48
|
+
|
|
49
|
+
- `product`
|
|
50
|
+
- `region`
|
|
51
|
+
- `fico_band`
|
|
52
|
+
- `ltv_band`
|
|
53
|
+
|
|
54
|
+
But inside those public buckets, hidden composition may vary by:
|
|
55
|
+
|
|
56
|
+
- broker channel
|
|
57
|
+
- employment type
|
|
58
|
+
- vintage
|
|
59
|
+
- hardship history
|
|
60
|
+
- documentation type
|
|
61
|
+
- local housing market
|
|
62
|
+
- borrower cashflow pattern
|
|
63
|
+
|
|
64
|
+
If those hidden subgroups have different expected-loss rates, the public
|
|
65
|
+
segmentation may not fully support the reported aggregate. The report quantifies
|
|
66
|
+
that hidden-composition ambiguity and identifies which hidden variables would
|
|
67
|
+
most improve the public segmentation.
|
|
68
|
+
|
|
69
|
+
## What The Report Separates
|
|
70
|
+
|
|
71
|
+
The package is intentionally narrow. It separates:
|
|
72
|
+
|
|
73
|
+
- reported risk estimate: the supplied metric, such as expected loss or default
|
|
74
|
+
rate
|
|
75
|
+
- statistical uncertainty: confidence intervals or model uncertainty supplied by
|
|
76
|
+
other workflows
|
|
77
|
+
- hidden-composition ambiguity: how far the reported metric can move when hidden
|
|
78
|
+
mix shifts inside fixed public buckets
|
|
79
|
+
- refinement recommendations: hidden fields that would make the public
|
|
80
|
+
representation more stable
|
|
81
|
+
|
|
82
|
+
This is not a confidence interval and not a full model-risk-management system.
|
|
83
|
+
It is a reviewable control for one practical question: whether the reporting
|
|
84
|
+
representation is stable enough for the risk metric.
|
|
85
|
+
|
|
86
|
+
## Analyst Workflow
|
|
87
|
+
|
|
88
|
+
1. Choose public buckets from the model report.
|
|
89
|
+
2. Choose hidden refinements that are available internally but not shown in the
|
|
90
|
+
public segmentation.
|
|
91
|
+
3. Choose the target risk metric.
|
|
92
|
+
4. Choose a plausible hidden-mix shift preset.
|
|
93
|
+
5. Set a review threshold for hidden-composition ambiguity.
|
|
94
|
+
6. Attach the generated Markdown report to a model-review or monitoring pack.
|
|
95
|
+
|
|
96
|
+
The review status is deliberately simple:
|
|
97
|
+
|
|
98
|
+
- `pass`: ambiguity and public adequacy checks are within the chosen thresholds
|
|
99
|
+
- `attention required`: the public segmentation may need refinement or explicit
|
|
100
|
+
acceptance of the ambiguity band
|
|
101
|
+
|
|
102
|
+
## Example
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import updatesupport_finance as usf
|
|
106
|
+
|
|
107
|
+
report = usf.model_risk_report(
|
|
108
|
+
portfolio,
|
|
109
|
+
public=["product", "region", "fico_band", "ltv_band"],
|
|
110
|
+
hidden=[
|
|
111
|
+
"product",
|
|
112
|
+
"region",
|
|
113
|
+
"fico_band",
|
|
114
|
+
"ltv_band",
|
|
115
|
+
"broker_channel",
|
|
116
|
+
"employment_type",
|
|
117
|
+
"vintage",
|
|
118
|
+
],
|
|
119
|
+
metric=usf.expected_loss(pd="pd", lgd="lgd"),
|
|
120
|
+
exposure="ead",
|
|
121
|
+
q=usf.q_portfolio_mix_shift(radius=0.25),
|
|
122
|
+
model_id="EL_RETAIL_2026Q2",
|
|
123
|
+
portfolio_name="Retail credit portfolio",
|
|
124
|
+
as_of_date="2026-06-30",
|
|
125
|
+
intended_use="Expected-loss segmentation model review",
|
|
126
|
+
ambiguity_limit=0.0025,
|
|
127
|
+
public_adequacy_required=False,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
print(report.to_markdown())
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The report answers:
|
|
134
|
+
|
|
135
|
+
- What is the reported portfolio risk estimate?
|
|
136
|
+
- What range is still possible under hidden mix shifts?
|
|
137
|
+
- Does the ambiguity exceed the review threshold?
|
|
138
|
+
- Which public buckets drive the instability?
|
|
139
|
+
- Which hidden fields are most valuable as public refinements?
|
|
140
|
+
- Which small public segmentation sits on the stability frontier, and why did
|
|
141
|
+
it beat nearby alternatives?
|
|
142
|
+
|
|
143
|
+
A synthetic portfolio example is available in `examples/model_risk_portfolio.py`
|
|
144
|
+
in the source repository:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
uv run --package updatesupport-finance python \
|
|
148
|
+
packages/updatesupport-finance/examples/model_risk_portfolio.py
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The example prints both the finance model-risk report and a core
|
|
152
|
+
`public_representation_frontier(...)` report for the same expected-loss metric.
|
|
153
|
+
The frontier section compares baseline versus selected ambiguity, close
|
|
154
|
+
dominated alternatives, and any screened-out refinement fields.
|
|
155
|
+
|
|
156
|
+
To write the Markdown report:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uv run --package updatesupport-finance python \
|
|
160
|
+
packages/updatesupport-finance/examples/model_risk_portfolio.py \
|
|
161
|
+
--output data/finance_model_risk_report.md
|
|
162
|
+
```
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Synthetic financial model-risk report example.
|
|
2
|
+
|
|
3
|
+
Run from the repository root with:
|
|
4
|
+
|
|
5
|
+
uv run --package updatesupport-finance python \
|
|
6
|
+
packages/updatesupport-finance/examples/model_risk_portfolio.py
|
|
7
|
+
|
|
8
|
+
Optionally write the Markdown report:
|
|
9
|
+
|
|
10
|
+
uv run --package updatesupport-finance python \
|
|
11
|
+
packages/updatesupport-finance/examples/model_risk_portfolio.py \
|
|
12
|
+
--output data/finance_model_risk_report.md
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import updatesupport as us
|
|
22
|
+
import updatesupport_finance as usf
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
PUBLIC_COLUMNS = ("product", "region", "fico_band", "ltv_band")
|
|
26
|
+
HIDDEN_COLUMNS = (
|
|
27
|
+
"product",
|
|
28
|
+
"region",
|
|
29
|
+
"fico_band",
|
|
30
|
+
"ltv_band",
|
|
31
|
+
"broker_channel",
|
|
32
|
+
"employment_type",
|
|
33
|
+
"vintage",
|
|
34
|
+
"hardship_history",
|
|
35
|
+
"documentation_type",
|
|
36
|
+
"local_housing_market",
|
|
37
|
+
"cashflow_pattern",
|
|
38
|
+
)
|
|
39
|
+
CANDIDATE_REFINEMENTS = (
|
|
40
|
+
"broker_channel",
|
|
41
|
+
"employment_type",
|
|
42
|
+
"vintage",
|
|
43
|
+
"hardship_history",
|
|
44
|
+
"documentation_type",
|
|
45
|
+
"local_housing_market",
|
|
46
|
+
"cashflow_pattern",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def synthetic_portfolio_rows() -> list[dict[str, Any]]:
|
|
51
|
+
"""Return a small synthetic retail-credit portfolio.
|
|
52
|
+
|
|
53
|
+
The public reporting segmentation is intentionally coarse. Several hidden
|
|
54
|
+
subgroups live inside the same public buckets and have different expected
|
|
55
|
+
loss rates, which makes the representation-stability question visible.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
"account_count": 140,
|
|
61
|
+
"product": "mortgage",
|
|
62
|
+
"region": "north",
|
|
63
|
+
"fico_band": "prime",
|
|
64
|
+
"ltv_band": "low",
|
|
65
|
+
"broker_channel": "broker",
|
|
66
|
+
"employment_type": "salaried",
|
|
67
|
+
"vintage": "2024",
|
|
68
|
+
"hardship_history": "none",
|
|
69
|
+
"documentation_type": "full_doc",
|
|
70
|
+
"local_housing_market": "stable",
|
|
71
|
+
"cashflow_pattern": "stable",
|
|
72
|
+
"pd": 0.014,
|
|
73
|
+
"lgd": 0.32,
|
|
74
|
+
"ead": 15_400_000,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"account_count": 90,
|
|
78
|
+
"product": "mortgage",
|
|
79
|
+
"region": "north",
|
|
80
|
+
"fico_band": "prime",
|
|
81
|
+
"ltv_band": "low",
|
|
82
|
+
"broker_channel": "direct",
|
|
83
|
+
"employment_type": "self_employed",
|
|
84
|
+
"vintage": "2023",
|
|
85
|
+
"hardship_history": "prior",
|
|
86
|
+
"documentation_type": "alt_doc",
|
|
87
|
+
"local_housing_market": "cooling",
|
|
88
|
+
"cashflow_pattern": "seasonal",
|
|
89
|
+
"pd": 0.038,
|
|
90
|
+
"lgd": 0.46,
|
|
91
|
+
"ead": 8_100_000,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"account_count": 110,
|
|
95
|
+
"product": "mortgage",
|
|
96
|
+
"region": "north",
|
|
97
|
+
"fico_band": "prime",
|
|
98
|
+
"ltv_band": "medium",
|
|
99
|
+
"broker_channel": "broker",
|
|
100
|
+
"employment_type": "salaried",
|
|
101
|
+
"vintage": "2022",
|
|
102
|
+
"hardship_history": "none",
|
|
103
|
+
"documentation_type": "full_doc",
|
|
104
|
+
"local_housing_market": "cooling",
|
|
105
|
+
"cashflow_pattern": "stable",
|
|
106
|
+
"pd": 0.023,
|
|
107
|
+
"lgd": 0.39,
|
|
108
|
+
"ead": 12_650_000,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"account_count": 70,
|
|
112
|
+
"product": "mortgage",
|
|
113
|
+
"region": "north",
|
|
114
|
+
"fico_band": "prime",
|
|
115
|
+
"ltv_band": "medium",
|
|
116
|
+
"broker_channel": "correspondent",
|
|
117
|
+
"employment_type": "contractor",
|
|
118
|
+
"vintage": "2021",
|
|
119
|
+
"hardship_history": "prior",
|
|
120
|
+
"documentation_type": "full_doc",
|
|
121
|
+
"local_housing_market": "declining",
|
|
122
|
+
"cashflow_pattern": "volatile",
|
|
123
|
+
"pd": 0.049,
|
|
124
|
+
"lgd": 0.52,
|
|
125
|
+
"ead": 7_700_000,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"account_count": 85,
|
|
129
|
+
"product": "mortgage",
|
|
130
|
+
"region": "south",
|
|
131
|
+
"fico_band": "near_prime",
|
|
132
|
+
"ltv_band": "high",
|
|
133
|
+
"broker_channel": "broker",
|
|
134
|
+
"employment_type": "salaried",
|
|
135
|
+
"vintage": "2024",
|
|
136
|
+
"hardship_history": "none",
|
|
137
|
+
"documentation_type": "full_doc",
|
|
138
|
+
"local_housing_market": "stable",
|
|
139
|
+
"cashflow_pattern": "stable",
|
|
140
|
+
"pd": 0.058,
|
|
141
|
+
"lgd": 0.56,
|
|
142
|
+
"ead": 7_225_000,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"account_count": 65,
|
|
146
|
+
"product": "mortgage",
|
|
147
|
+
"region": "south",
|
|
148
|
+
"fico_band": "near_prime",
|
|
149
|
+
"ltv_band": "high",
|
|
150
|
+
"broker_channel": "correspondent",
|
|
151
|
+
"employment_type": "self_employed",
|
|
152
|
+
"vintage": "2022",
|
|
153
|
+
"hardship_history": "current",
|
|
154
|
+
"documentation_type": "alt_doc",
|
|
155
|
+
"local_housing_market": "declining",
|
|
156
|
+
"cashflow_pattern": "volatile",
|
|
157
|
+
"pd": 0.118,
|
|
158
|
+
"lgd": 0.67,
|
|
159
|
+
"ead": 5_525_000,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"account_count": 160,
|
|
163
|
+
"product": "auto",
|
|
164
|
+
"region": "west",
|
|
165
|
+
"fico_band": "prime",
|
|
166
|
+
"ltv_band": "medium",
|
|
167
|
+
"broker_channel": "dealer",
|
|
168
|
+
"employment_type": "salaried",
|
|
169
|
+
"vintage": "2024",
|
|
170
|
+
"hardship_history": "none",
|
|
171
|
+
"documentation_type": "full_doc",
|
|
172
|
+
"local_housing_market": "stable",
|
|
173
|
+
"cashflow_pattern": "stable",
|
|
174
|
+
"pd": 0.031,
|
|
175
|
+
"lgd": 0.50,
|
|
176
|
+
"ead": 3_840_000,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"account_count": 100,
|
|
180
|
+
"product": "auto",
|
|
181
|
+
"region": "west",
|
|
182
|
+
"fico_band": "prime",
|
|
183
|
+
"ltv_band": "medium",
|
|
184
|
+
"broker_channel": "online",
|
|
185
|
+
"employment_type": "contractor",
|
|
186
|
+
"vintage": "2023",
|
|
187
|
+
"hardship_history": "prior",
|
|
188
|
+
"documentation_type": "stated_income",
|
|
189
|
+
"local_housing_market": "stable",
|
|
190
|
+
"cashflow_pattern": "seasonal",
|
|
191
|
+
"pd": 0.061,
|
|
192
|
+
"lgd": 0.58,
|
|
193
|
+
"ead": 2_400_000,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"account_count": 190,
|
|
197
|
+
"product": "card",
|
|
198
|
+
"region": "east",
|
|
199
|
+
"fico_band": "near_prime",
|
|
200
|
+
"ltv_band": "na",
|
|
201
|
+
"broker_channel": "direct",
|
|
202
|
+
"employment_type": "salaried",
|
|
203
|
+
"vintage": "2024",
|
|
204
|
+
"hardship_history": "none",
|
|
205
|
+
"documentation_type": "full_doc",
|
|
206
|
+
"local_housing_market": "stable",
|
|
207
|
+
"cashflow_pattern": "stable",
|
|
208
|
+
"pd": 0.074,
|
|
209
|
+
"lgd": 0.82,
|
|
210
|
+
"ead": 1_900_000,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"account_count": 120,
|
|
214
|
+
"product": "card",
|
|
215
|
+
"region": "east",
|
|
216
|
+
"fico_band": "near_prime",
|
|
217
|
+
"ltv_band": "na",
|
|
218
|
+
"broker_channel": "affiliate",
|
|
219
|
+
"employment_type": "contractor",
|
|
220
|
+
"vintage": "2022",
|
|
221
|
+
"hardship_history": "current",
|
|
222
|
+
"documentation_type": "thin_file",
|
|
223
|
+
"local_housing_market": "cooling",
|
|
224
|
+
"cashflow_pattern": "volatile",
|
|
225
|
+
"pd": 0.132,
|
|
226
|
+
"lgd": 0.90,
|
|
227
|
+
"ead": 1_200_000,
|
|
228
|
+
},
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def build_report(
|
|
233
|
+
*,
|
|
234
|
+
q_radius: float = 0.35,
|
|
235
|
+
ambiguity_limit: float = 0.006,
|
|
236
|
+
) -> usf.ModelRiskReport:
|
|
237
|
+
return usf.model_risk_report(
|
|
238
|
+
synthetic_portfolio_rows(),
|
|
239
|
+
public=PUBLIC_COLUMNS,
|
|
240
|
+
hidden=HIDDEN_COLUMNS,
|
|
241
|
+
metric=usf.expected_loss(pd="pd", lgd="lgd"),
|
|
242
|
+
exposure="ead",
|
|
243
|
+
candidate_refinements=CANDIDATE_REFINEMENTS,
|
|
244
|
+
q=usf.q_portfolio_mix_shift(radius=q_radius),
|
|
245
|
+
model_id="EL_SYNTHETIC_RETAIL_001",
|
|
246
|
+
portfolio_name="Synthetic retail credit portfolio",
|
|
247
|
+
as_of_date="2026-06-30",
|
|
248
|
+
intended_use="Expected-loss segmentation model review",
|
|
249
|
+
ambiguity_limit=ambiguity_limit,
|
|
250
|
+
public_adequacy_required=False,
|
|
251
|
+
top=5,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def build_frontier(
|
|
256
|
+
*,
|
|
257
|
+
q_radius: float = 0.35,
|
|
258
|
+
ambiguity_limit: float = 0.006,
|
|
259
|
+
) -> us.PublicRepresentationFrontier:
|
|
260
|
+
"""Choose the smallest stable public segmentation for the portfolio metric."""
|
|
261
|
+
|
|
262
|
+
return us.public_representation_frontier(
|
|
263
|
+
synthetic_portfolio_rows(),
|
|
264
|
+
base_public=PUBLIC_COLUMNS,
|
|
265
|
+
hidden=HIDDEN_COLUMNS,
|
|
266
|
+
target=usf.expected_loss(pd="pd", lgd="lgd"),
|
|
267
|
+
weight="ead",
|
|
268
|
+
candidate_refinements=CANDIDATE_REFINEMENTS,
|
|
269
|
+
q_presets=(
|
|
270
|
+
"saturated",
|
|
271
|
+
usf.q_portfolio_mix_shift(radius=q_radius),
|
|
272
|
+
"observed",
|
|
273
|
+
),
|
|
274
|
+
min_cell_weights=(1.0,),
|
|
275
|
+
ambiguity_limit=ambiguity_limit,
|
|
276
|
+
bucket_budget=12,
|
|
277
|
+
search="beam",
|
|
278
|
+
beam_width=8,
|
|
279
|
+
max_added_columns=3,
|
|
280
|
+
max_evaluations=96,
|
|
281
|
+
title="Synthetic Finance Public Representation Frontier",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main() -> None:
|
|
286
|
+
parser = argparse.ArgumentParser(
|
|
287
|
+
description="Generate a synthetic finance model-risk report.",
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--output",
|
|
291
|
+
type=Path,
|
|
292
|
+
help="Optional Markdown output path.",
|
|
293
|
+
)
|
|
294
|
+
parser.add_argument(
|
|
295
|
+
"--q-radius",
|
|
296
|
+
type=float,
|
|
297
|
+
default=0.35,
|
|
298
|
+
help="Portfolio mix-shift radius for the bounded-shift Q preset.",
|
|
299
|
+
)
|
|
300
|
+
parser.add_argument(
|
|
301
|
+
"--ambiguity-limit",
|
|
302
|
+
type=float,
|
|
303
|
+
default=0.006,
|
|
304
|
+
help="Review threshold for hidden-composition ambiguity.",
|
|
305
|
+
)
|
|
306
|
+
args = parser.parse_args()
|
|
307
|
+
|
|
308
|
+
report = build_report(
|
|
309
|
+
q_radius=args.q_radius,
|
|
310
|
+
ambiguity_limit=args.ambiguity_limit,
|
|
311
|
+
)
|
|
312
|
+
frontier = build_frontier(
|
|
313
|
+
q_radius=args.q_radius,
|
|
314
|
+
ambiguity_limit=args.ambiguity_limit,
|
|
315
|
+
)
|
|
316
|
+
markdown = report.to_markdown() + "\n\n" + frontier.to_markdown()
|
|
317
|
+
if args.output is None:
|
|
318
|
+
print(markdown)
|
|
319
|
+
return
|
|
320
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
321
|
+
args.output.write_text(markdown + "\n", encoding="utf-8")
|
|
322
|
+
print(f"Wrote {args.output}")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
if __name__ == "__main__":
|
|
326
|
+
main()
|