axis-synome 0.1.0.dev29__tar.gz → 0.1.0.dev31__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.
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/PKG-INFO +1 -1
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/_version.py +2 -2
- axis_synome-0.1.0.dev31/src/axis_synome/spec/suraf/README.md +281 -0
- axis_synome-0.1.0.dev31/src/axis_synome/spec/suraf/entities/assessor_score.py +43 -0
- axis_synome-0.1.0.dev31/src/axis_synome/spec/suraf/entities/assets.py +48 -0
- axis_synome-0.1.0.dev31/src/axis_synome/spec/suraf/entities/mappings.py +60 -0
- axis_synome-0.1.0.dev31/src/axis_synome/spec/suraf/formulas/crr.py +54 -0
- axis_synome-0.1.0.dev31/src/axis_synome/spec/suraf/formulas/scoring.py +86 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome.egg-info/PKG-INFO +1 -1
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome.egg-info/SOURCES.txt +22 -1
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/__init__.py +0 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/entities/__init__.py +0 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/entities/test_assessor_score.py +33 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/formulas/__init__.py +0 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/formulas/test_crr.py +132 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/formulas/test_scoring.py +227 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/static/aave_ausdc/v1/crr_mapping.csv +18 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/static/aave_ausdc/v1/penalty.csv +4 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/static/aave_ausdc/v1/scorecards/Assessor_1_scores.csv +54 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/static/aave_ausdc/v1/scorecards/Assessor_2_scores.csv +54 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/static/aave_ausdc/v1/scorecards/Assessor_3_scores.csv +54 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/static/aave_ausdc/v1/weights.csv +54 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/suraf_client/__init__.py +0 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/suraf_client/suraf_client.py +256 -0
- axis_synome-0.1.0.dev31/tests/axis_synome/suraf/suraf_client/test_suraf_client.py +186 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/.flake8 +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/README.md +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/WRITING_SPECS.md +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/pyproject.toml +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/setup.cfg +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/README.md +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/entities/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/entities/assets.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/entities/assets_by_prime.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/entities/networks.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/entities/primes.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/entities/protocol_sets.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/entities/tokens.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/entities/types.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/formulas/asc.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/formulas/asc_collateral_ratio.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/formulas/asc_incentive.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/formulas/dab.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/formulas/latent_asc.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/formulas/ratio_latent_asc.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/asc/formulas/resting_asc.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/codegen_test/entities/agents.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/crypto_lending/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/crypto_lending/formulas/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/crypto_lending/formulas/lif.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/risk_capital/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/risk_capital/formulas/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec/risk_capital/formulas/required_risk_capital.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_support/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_support/evm_address.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_support/runtime/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_support/runtime/base.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_support/runtime/math.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_support/runtime/reference.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_support/validated_dataclass.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_support/validated_str.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_validator/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_validator/checker.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_validator/flake8_plugin.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome/spec_validator/python_subset.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome.egg-info/dependency_links.txt +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome.egg-info/entry_points.txt +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome.egg-info/requires.txt +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/src/axis_synome.egg-info/top_level.txt +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/conftest.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/mocks.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_asc.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_asc_collateral_ratio.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_asc_incentive.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_dab.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_evm_address.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_latent_asc.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_prime_agent_data_validation.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_ratio_latent_asc.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/asc/test_resting_asc.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/risk_capital/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/risk_capital/formulas/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/risk_capital/formulas/test_loss_given_default.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/spec_support/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/spec_support/runtime/__init__.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/spec_support/runtime/test_base.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/spec_support/runtime/test_math.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/spec_validator/test_checker.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/tests/axis_synome/spec_validator/test_flake8_plugin.py +0 -0
- {axis_synome-0.1.0.dev29 → axis_synome-0.1.0.dev31}/uv.lock +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, 0, '
|
|
21
|
+
__version__ = version = '0.1.0.dev31'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0, 'dev31')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# SURAF Specification
|
|
2
|
+
|
|
3
|
+
This module is the executable specification for Sky Protocol's **SURAF** collateral risk assessment framework. It encodes the scoring pipeline and Capital Requirement Ratio (CRR) derivation rules into typed Python so that assessments can be evaluated, tested, and audited against real scorecard data.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Background
|
|
8
|
+
|
|
9
|
+
Sky Protocol accepts collateral assets to back USDS issuance. The risk of each asset must be quantified before capital can be committed against it. **SURAF** is the framework that does this: three independent assessors score an asset across five pillars of risk, and the resulting aggregate score determines a **Capital Requirement Ratio (CRR)** — the fraction of exposure that must be held as reserve capital.
|
|
10
|
+
|
|
11
|
+
A higher CRR means the asset is riskier and demands more capital. A penalty is added to the base CRR when assessors flag structurally weak subsections with the lowest possible score (1 out of 5).
|
|
12
|
+
|
|
13
|
+
This specification answers: *given three completed scorecards and the governance-approved mapping tables, what is the adjusted CRR for this asset?*
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Module Layout
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
entities/ : data types and constants (inputs to the pipeline)
|
|
21
|
+
formulas/ : pure functions that compute the CRR from entities
|
|
22
|
+
opaque/ : implementation helpers exempt from the spec subset (e.g. interpolation via numpy)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
All formulas follow one rule: **they accept raw entity inputs and compute everything internally**. No formula accepts a precomputed intermediate value as an argument.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Entities
|
|
30
|
+
|
|
31
|
+
### `AssessorScore` (`entities/assessor_score.py`)
|
|
32
|
+
|
|
33
|
+
One assessor's complete evaluation of an asset. Subsection scores are grouped by pillar and stored together with their absolute weights.
|
|
34
|
+
|
|
35
|
+
| Field | Type | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `scores` | `list[PillarEntry]` | Scored pillars; each entry is `(pillar_weight, [(section_weight, score), ...])` |
|
|
38
|
+
|
|
39
|
+
Where `PillarEntry = tuple[PillarWeight, list[SectionEntry]]` and `SectionEntry = tuple[SubsectionWeight, SubsectionScore]`.
|
|
40
|
+
|
|
41
|
+
**Score range:** `SubsectionScore` ∈ [`1.0`, `5.0`].
|
|
42
|
+
|
|
43
|
+
**Weight semantics:** weights are **absolute integers** defined in the weights file, e.g. `40` for Pillar I and `8` for subsection 1.A. Normalisation (dividing by totals) happens inside the formula functions, not in the inputs.
|
|
44
|
+
|
|
45
|
+
Only scored subsections are included; callers must filter out blanks before construction.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### `EligibleSURAFAsset` (`entities/assets.py`)
|
|
50
|
+
|
|
51
|
+
A closed-world Enum of assets approved for SURAF assessment. Each member's `.value` is a fully specified `AssetWrapper` (token, network, protocol, on-chain address, Atlas source reference).
|
|
52
|
+
|
|
53
|
+
| Member | Asset |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `AAVE_AUSDC` | Aave aUSDC on Ethereum Mainnet via Aave Core V3 |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### `CRRMapping` (`entities/mappings.py`)
|
|
60
|
+
|
|
61
|
+
Governance-approved score-to-CRR% interpolation table. Maps an aggregate score (1–5) to an unadjusted Capital Requirement Ratio (%). Linear interpolation between breakpoints; values outside the range are clamped.
|
|
62
|
+
|
|
63
|
+
| Field | Type | Description |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `table` | `tuple[tuple[float, float], ...]` | Ordered `(score, crr_pct)` breakpoints; scores strictly increasing |
|
|
66
|
+
|
|
67
|
+
Example from the reference table:
|
|
68
|
+
|
|
69
|
+
| Score | CRR% |
|
|
70
|
+
|---|---|
|
|
71
|
+
| 1.00 | 100 |
|
|
72
|
+
| 2.50 | 28 |
|
|
73
|
+
| 3.00 | 20 |
|
|
74
|
+
| 5.00 | 5 |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
### `PenaltyMapping` (`entities/mappings.py`)
|
|
79
|
+
|
|
80
|
+
Governance-approved count-to-penalty interpolation table. Maps the total number of score-1 subsections across all assessors to a penalty in percentage points added to the base CRR.
|
|
81
|
+
|
|
82
|
+
| Field | Type | Description |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `table` | `tuple[tuple[int, float], ...]` | Ordered `(n_score_1, penalty_pp)` breakpoints; counts non-negative and strictly increasing |
|
|
85
|
+
|
|
86
|
+
Example: 7 subsections scored 1 → `penalty_pp = 10 + (7 − 5) / (10 − 5) × (20 − 10) = 14.0 pp`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Formulas
|
|
91
|
+
|
|
92
|
+
### Top-level: `crr` (`formulas/crr.py`)
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
crr(asset, assessor_scores, crr_mapping, penalty_mapping) -> float
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Computes the adjusted Capital Requirement Ratio for a SURAF asset. Returns a value in `[0, 100]`, capped at 100%.
|
|
99
|
+
|
|
100
|
+
**Pipeline:**
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
1. overall_score(assessor) per-assessor two-level weighted average (section → pillar → overall)
|
|
104
|
+
2. weighted_average(assessors) arithmetic mean of all assessors' overall scores → avg_score
|
|
105
|
+
3. map_crr(avg_score, ...) look up base CRR% by linear interpolation
|
|
106
|
+
4. total_n_score_1(assessors) count of all score-1 subsections across all assessors
|
|
107
|
+
5. calculate_penalty(n, ...) look up penalty pp by linear interpolation
|
|
108
|
+
6. min(base_crr + penalty, 100) final adjusted CRR, capped at 100%
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Scoring helpers (`formulas/scoring.py`)
|
|
114
|
+
|
|
115
|
+
These pure functions implement each stage of the pipeline. They are composed by `crr`.
|
|
116
|
+
|
|
117
|
+
#### `overall_score(assessor)` → `float`
|
|
118
|
+
|
|
119
|
+
Two-level weighted average for a single assessor.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
pillar_score_i = Σ(section_weight_j × score_j) / Σ(section_weight_j)
|
|
123
|
+
overall = Σ(pillar_score_i × pillar_weight_i) / Σ(pillar_weight_i)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
#### `weighted_average(assessors)` → `float`
|
|
129
|
+
|
|
130
|
+
Arithmetic mean of `overall_score` across all assessors.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
#### `n_score_1(assessor)` → `int`
|
|
135
|
+
|
|
136
|
+
Count of subsections where an assessor gave the minimum score of `1`. Used to measure how many structurally weak areas the assessor flagged.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
#### `total_n_score_1(assessors)` → `int`
|
|
141
|
+
|
|
142
|
+
Sum of `n_score_1` across all assessors. This is the input to the penalty interpolation.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
#### `map_crr(avg_score, crr_mapping)` → `float`
|
|
148
|
+
|
|
149
|
+
Interpolates the base CRR% from the aggregate score using the `CRRMapping` table.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
#### `calculate_penalty(n_score1, penalty_mapping)` → `float`
|
|
154
|
+
|
|
155
|
+
Interpolates the penalty in percentage points from the score-1 count using the `PenaltyMapping` table.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Loading Data from CSVs
|
|
160
|
+
|
|
161
|
+
The spec operates on fully-constructed dataclasses. To go from raw CSV files (assessor scorecards, weights, mapping tables) to spec inputs, use the loader classes in `tests/axis_synome/suraf/suraf_client/suraf_client.py`.
|
|
162
|
+
|
|
163
|
+
> **Note:** these loaders are test utilities; they live in `tests/` because CSV I/O is a runtime concern, not a spec concern. They are the reference implementation for how data should be adapted before being passed to spec formulas.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `AssessorScoreInput.from_csv(path, weights_path)`
|
|
168
|
+
|
|
169
|
+
Parses an assessor scorecard CSV together with the weights CSV and returns an `AssessorScore` with absolute weights.
|
|
170
|
+
|
|
171
|
+
**Scorecard CSV** (`scorecards/Assessor_N_scores.csv`) — required columns: `pillar`, `subsection_ref`, `score`:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
pillar,subsection_ref,title,score
|
|
175
|
+
,,SECTION A: GENERAL INFORMATION,
|
|
176
|
+
1,1.A,Asset Composition & Eligibility,4
|
|
177
|
+
1,1.B,Asset Cashflow Structure,3
|
|
178
|
+
1,1.C,Asset Liquidity Analysis,
|
|
179
|
+
2,2.A,Recoverability & Custody,5
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Rows with a blank `score` column are skipped. Rows where `pillar` is not a digit (e.g. section headers) are skipped.
|
|
183
|
+
|
|
184
|
+
**Weights CSV** (`weights.csv`) — required columns: `pillar`, `subsection_ref`, `title`, `pillar_weight`, `subsection_weight`:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
pillar,subsection_ref,title,pillar_weight,subsection_weight
|
|
188
|
+
,,PILLAR I: ASSET & COLLATERAL ASSESSMENT,40,0
|
|
189
|
+
1,1.A,Asset Composition & Eligibility,0,8
|
|
190
|
+
1,1.B,Asset Cashflow Structure,0,7
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from suraf_client import AssessorScoreInput
|
|
195
|
+
|
|
196
|
+
a1 = AssessorScoreInput.from_csv("scorecards/Assessor_1_scores.csv", "weights.csv")
|
|
197
|
+
a2 = AssessorScoreInput.from_csv("scorecards/Assessor_2_scores.csv", "weights.csv")
|
|
198
|
+
a3 = AssessorScoreInput.from_csv("scorecards/Assessor_3_scores.csv", "weights.csv")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### `CRRMappingInput.from_csv(path)`
|
|
204
|
+
|
|
205
|
+
Parses a CRR interpolation table CSV and returns a `CRRMapping`.
|
|
206
|
+
|
|
207
|
+
**CSV** (`crr_mapping.csv`) — required columns: `score`, `crr`:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
score,crr
|
|
211
|
+
1.00,100
|
|
212
|
+
2.50,28
|
|
213
|
+
5.00,5
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Scores must be strictly increasing. At least two breakpoints are required.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from suraf_client import CRRMappingInput
|
|
220
|
+
|
|
221
|
+
crr_mapping = CRRMappingInput.from_csv("crr_mapping.csv")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### `PenaltyMappingInput.from_csv(path)`
|
|
227
|
+
|
|
228
|
+
Parses a penalty interpolation table CSV and returns a `PenaltyMapping`.
|
|
229
|
+
|
|
230
|
+
**CSV** (`penalty.csv`) — required columns: `n_score_1`, `penalty_pp`:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
n_score_1,penalty_pp
|
|
234
|
+
0,0
|
|
235
|
+
2,0
|
|
236
|
+
20,30
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`n_score_1` values must be non-negative and strictly increasing.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from suraf_client import PenaltyMappingInput
|
|
243
|
+
|
|
244
|
+
penalty_mapping = PenaltyMappingInput.from_csv("penalty.csv")
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
### End-to-end example
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from suraf_client import AssessorScoreInput, CRRMappingInput, PenaltyMappingInput
|
|
253
|
+
from axis_synome.spec.suraf.entities.assets import EligibleSURAFAsset
|
|
254
|
+
from axis_synome.spec.suraf.formulas.crr import crr
|
|
255
|
+
|
|
256
|
+
base = "tests/axis_synome/suraf/static/aave_ausdc/v1"
|
|
257
|
+
|
|
258
|
+
assessors = [
|
|
259
|
+
AssessorScoreInput.from_csv(f"{base}/scorecards/Assessor_{i}_scores.csv", f"{base}/weights.csv")
|
|
260
|
+
for i in range(1, 4)
|
|
261
|
+
]
|
|
262
|
+
crr_mapping = CRRMappingInput.from_csv(f"{base}/crr_mapping.csv")
|
|
263
|
+
penalty_map = PenaltyMappingInput.from_csv(f"{base}/penalty.csv")
|
|
264
|
+
|
|
265
|
+
result = crr(EligibleSURAFAsset.AAVE_AUSDC_DEMO, assessors, crr_mapping, penalty_map)
|
|
266
|
+
print(f"Adjusted CRR: {result:.2f}%")
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Design Principles
|
|
272
|
+
|
|
273
|
+
1. **No precomputed inputs.** Every formula accepts raw entities and derives all intermediates internally. This makes formulas independently testable and prevents stale-input bugs.
|
|
274
|
+
|
|
275
|
+
2. **Absolute weights, normalised internally.** Weights (e.g. `40` for Pillar I, `8` for subsection 1.A) are stored as absolute integers on `AssessorScore`. Each formula divides by the sum of weights at its level — pillar normalisation inside `overall_score`, assessor averaging inside `weighted_average`.
|
|
276
|
+
|
|
277
|
+
3. **Governance tables are entity inputs.** `CRRMapping` and `PenaltyMapping` are dataclass inputs, not hardcoded in formulas. Updating the governance decision means providing a new table, not changing spec code.
|
|
278
|
+
|
|
279
|
+
4. **Score-1 penalty is additive.** The penalty for flagged subsections (score = 1) is added to the base CRR after interpolation and the total is capped at `100%`. This is a separate governance lever from the score-to-CRR curve.
|
|
280
|
+
|
|
281
|
+
5. **Opaque functions are isolated.** Numerical primitives that require external libraries (e.g. `numpy.interp` for linear interpolation) live in `opaque/` and are exempt from the spec language subset. The spec formulas call them by name; the parser treats the call as an uninterpreted function.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from axis_synome.spec_support.validated_dataclass import validated_dataclass
|
|
6
|
+
|
|
7
|
+
SCORE_MIN: int = 1
|
|
8
|
+
SCORE_MAX: int = 5
|
|
9
|
+
|
|
10
|
+
SubsectionScore = Annotated[int, Field(ge=SCORE_MIN, le=SCORE_MAX)]
|
|
11
|
+
SubsectionWeight = Annotated[int, Field(gt=0)]
|
|
12
|
+
PillarWeight = Annotated[int, Field(gt=0)]
|
|
13
|
+
|
|
14
|
+
SectionEntry = tuple[SubsectionWeight, SubsectionScore]
|
|
15
|
+
PillarEntry = tuple[PillarWeight, list[SectionEntry]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@validated_dataclass
|
|
19
|
+
class AssessorScore:
|
|
20
|
+
"""One assessor's subsection-level evaluation of a SURAF asset.
|
|
21
|
+
|
|
22
|
+
``scores`` groups subsection evaluations by pillar. Each entry is a
|
|
23
|
+
``(pillar_weight, sections)`` pair where:
|
|
24
|
+
|
|
25
|
+
* ``pillar_weight`` – absolute weight of the pillar (e.g. 40 for Pillar I).
|
|
26
|
+
* ``sections`` – list of ``(section_weight, score)`` pairs for every
|
|
27
|
+
scored subsection within that pillar, where
|
|
28
|
+
``section_weight`` is the absolute subsection weight
|
|
29
|
+
(e.g. 8) and ``score`` ∈ [``SCORE_MIN``, ``SCORE_MAX``].
|
|
30
|
+
|
|
31
|
+
Only scored (non-null) subsections are included; callers must filter out
|
|
32
|
+
unscored subsections before constructing this object.
|
|
33
|
+
|
|
34
|
+
The two-level weighted average for this assessor is::
|
|
35
|
+
|
|
36
|
+
pillar_score_i = Σ(score_j × section_w_j) / Σ(section_w_j)
|
|
37
|
+
overall = Σ(pillar_score_i × pillar_w_i) / Σ(pillar_w_i)
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
scores: Absolute-weight-annotated subsection evaluations grouped by pillar.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
scores: list[PillarEntry]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from dataclasses import KW_ONLY
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from axis_synome.spec.asc.entities.assets import Asset
|
|
5
|
+
from axis_synome.spec.asc.entities.networks import Network
|
|
6
|
+
from axis_synome.spec.asc.entities.protocol_sets import Protocol
|
|
7
|
+
from axis_synome.spec.asc.entities.tokens import Token
|
|
8
|
+
from axis_synome.spec_support.evm_address import EvmAddress
|
|
9
|
+
from axis_synome.spec_support.validated_dataclass import validated_dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@validated_dataclass
|
|
13
|
+
class AssetWrapper(Asset):
|
|
14
|
+
"""Wrapper around Asset to include SURAF-specific fields (i.e halo identifiers, prime's link to the asset, etc).
|
|
15
|
+
Placeholder until we have a more complete SURAF asset spec.
|
|
16
|
+
|
|
17
|
+
TODO: Investigate if there are shareable entities between different specs (e.g. Asset) and move to a common module
|
|
18
|
+
to avoid duplication. For now, we can keep SURAF-specific fields in a wrapper class that inherit from asc/assets
|
|
19
|
+
until we have a more complete SURAF asset spec.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_: KW_ONLY
|
|
24
|
+
halo_id: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SURAFAsset(Enum):
|
|
28
|
+
"""All SURAF assets."""
|
|
29
|
+
|
|
30
|
+
AAVE_AUSDC_DEMO = AssetWrapper(
|
|
31
|
+
token=Token.A_ETH_USDC,
|
|
32
|
+
network=Network.ETHEREUM_MAINNET,
|
|
33
|
+
protocol=Protocol.AAVE_CORE_V3,
|
|
34
|
+
address=EvmAddress("0x98C23E9d8f34FEFb1B7BD6a91B7FF122F4e16F5c"),
|
|
35
|
+
# TODO: include url/ uuid
|
|
36
|
+
source="A.6.1.1.2.2.6.1.3.1.5.1.2.2.1",
|
|
37
|
+
underlying_asset=Token.A_ETH_USDC,
|
|
38
|
+
underlying_asset_address=EvmAddress("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
|
|
39
|
+
underlying_asset_source="A.6.1.1.2.2.6.1.3.1.5.1.2.2.2",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EligibleSURAFAsset(Enum):
|
|
44
|
+
"""SURAF assets eligible for CRR calculation.
|
|
45
|
+
We are only supporting a demo asset for now.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
AAVE_AUSDC_DEMO = SURAFAsset.AAVE_AUSDC_DEMO.value
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from axis_synome.spec_support.validated_dataclass import validated_dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@validated_dataclass
|
|
5
|
+
class CRRMapping:
|
|
6
|
+
"""Score → CRR% interpolation table.
|
|
7
|
+
|
|
8
|
+
Maps an aggregate assessor score (1–5) to an unadjusted Capital
|
|
9
|
+
Requirement Ratio (%). Interpolation is linear between breakpoints;
|
|
10
|
+
values outside the range are clamped to the first/last CRR value.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
table: Ordered breakpoints as ``((score, crr_pct), ...)``.
|
|
14
|
+
Scores must be strictly increasing; CRR values are in percent.
|
|
15
|
+
|
|
16
|
+
Example::
|
|
17
|
+
|
|
18
|
+
CRRMapping(
|
|
19
|
+
table=(
|
|
20
|
+
(1.0, 100.0),
|
|
21
|
+
(2.5, 60.0),
|
|
22
|
+
(4.0, 20.0),
|
|
23
|
+
(5.0, 5.0),
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
# avg_score=3.25 interpolates between (2.5, 60) and (4.0, 20):
|
|
27
|
+
# crr = 60 + (3.25 - 2.5) / (4.0 - 2.5) * (20 - 60) = 40.0 %
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
table: tuple[tuple[float, float], ...]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@validated_dataclass
|
|
34
|
+
class PenaltyMapping:
|
|
35
|
+
"""Score-1 count → penalty percentage-point interpolation table.
|
|
36
|
+
|
|
37
|
+
Maps the total number of score-1 subsections across all assessors to a
|
|
38
|
+
penalty in percentage points added to the unadjusted CRR. More score-1
|
|
39
|
+
subsections → higher penalty → higher capital charge.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
table: Ordered breakpoints as ``((n_score_1, penalty_pp), ...)``.
|
|
43
|
+
``n_score_1`` values must be non-negative and strictly
|
|
44
|
+
increasing.
|
|
45
|
+
|
|
46
|
+
Example::
|
|
47
|
+
|
|
48
|
+
PenaltyMapping(
|
|
49
|
+
table=(
|
|
50
|
+
( 0, 0.0),
|
|
51
|
+
( 5, 10.0),
|
|
52
|
+
(10, 20.0),
|
|
53
|
+
(20, 30.0),
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
# 7 total score-1 subsections interpolates between (5, 10) and (10, 20):
|
|
57
|
+
# penalty_pp = 10 + (7 - 5) / (10 - 5) * (20 - 10) = 14.0 pp
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
table: tuple[tuple[int, float], ...]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from axis_synome.spec.suraf.entities.assessor_score import AssessorScore
|
|
2
|
+
from axis_synome.spec.suraf.entities.assets import EligibleSURAFAsset
|
|
3
|
+
from axis_synome.spec.suraf.entities.mappings import CRRMapping, PenaltyMapping
|
|
4
|
+
from axis_synome.spec.suraf.formulas.scoring import (
|
|
5
|
+
calculate_penalty,
|
|
6
|
+
map_crr,
|
|
7
|
+
total_n_score_1,
|
|
8
|
+
weighted_average,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def crr(
|
|
13
|
+
_asset: EligibleSURAFAsset,
|
|
14
|
+
assessor_scores: list[AssessorScore],
|
|
15
|
+
crr_mapping: CRRMapping,
|
|
16
|
+
penalty_mapping: PenaltyMapping,
|
|
17
|
+
) -> float:
|
|
18
|
+
"""Compute the adjusted Capital Requirement Ratio (CRR) for a SURAF asset
|
|
19
|
+
:source_uuid: 3828778e-0197-4ce9-a836-6770d04f2ea9
|
|
20
|
+
|
|
21
|
+
The CRR reflects the credit and operational risk of the collateral asset and
|
|
22
|
+
determines the required reserve capital fraction for a given USD exposure.
|
|
23
|
+
|
|
24
|
+
Pipeline
|
|
25
|
+
--------
|
|
26
|
+
1. Each assessor's overall score is a two-level weighted average: subsection
|
|
27
|
+
scores are weighted by their absolute section weights within each pillar,
|
|
28
|
+
then pillar scores are weighted by their absolute pillar weights.
|
|
29
|
+
Weights are absolute integers (e.g. 40 for Pillar I, 8 for subsection 1.A);
|
|
30
|
+
normalisation is done internally by ``overall_score``.
|
|
31
|
+
2. The assessors' overall scores are averaged to produce ``avg_score``.
|
|
32
|
+
3. ``avg_score`` is mapped to an unadjusted CRR via ``crr_mapping``
|
|
33
|
+
(linear interpolation).
|
|
34
|
+
4. A penalty in percentage points is derived from the total number of
|
|
35
|
+
score-1 subsections across all assessors via ``penalty_mapping``
|
|
36
|
+
(linear interpolation).
|
|
37
|
+
5. ``adjusted_crr = min(unadjusted_crr + penalty_pp, 100.0)``.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
_asset: The eligible SURAF asset being assessed. Placeholder to allow for future asset-specific logic (i.e specific mappings defined in axis-synome not in client side); currently unused.
|
|
41
|
+
assessor_scores: One :class:`AssessorScore` per independent assessor
|
|
42
|
+
(typically three). Blank subsections must already be
|
|
43
|
+
filtered out before constructing each instance.
|
|
44
|
+
crr_mapping: Score → CRR% interpolation table.
|
|
45
|
+
penalty_mapping: n_score_1 → penalty pp interpolation table.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Adjusted CRR as a percentage in ``[0, 100]``.
|
|
49
|
+
"""
|
|
50
|
+
return min(
|
|
51
|
+
map_crr(weighted_average(assessor_scores), crr_mapping)
|
|
52
|
+
+ calculate_penalty(total_n_score_1(assessor_scores), penalty_mapping),
|
|
53
|
+
100.0,
|
|
54
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""SURAF scoring pipeline as pure functions.
|
|
2
|
+
|
|
3
|
+
Scoring hierarchy
|
|
4
|
+
-----------------
|
|
5
|
+
1. Subsection level – AssessorScore.scores is a list of (pillar_weight, sections)
|
|
6
|
+
pairs where sections = [(section_weight, score), ...].
|
|
7
|
+
All weights are absolute (e.g. 40 for Pillar I, 8 for 1.A).
|
|
8
|
+
2. Overall score – two-level weighted average:
|
|
9
|
+
pillar_score_i = Σ(section_weight_j * score_j) / Σ(section_weight_j)
|
|
10
|
+
overall = Σ(pillar_score_i * pillar_weight_i) / Σ(pillar_weight_i)
|
|
11
|
+
3. Aggregate – simple arithmetic mean of the three assessors' overall
|
|
12
|
+
scores.
|
|
13
|
+
4. CRR – interpolated from CRRMapping.
|
|
14
|
+
5. Penalty – interpolated from PenaltyMapping; Added to CRR.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from axis_synome.spec.suraf.entities.assessor_score import AssessorScore, SectionEntry
|
|
18
|
+
from axis_synome.spec.suraf.entities.mappings import CRRMapping, PenaltyMapping
|
|
19
|
+
from axis_synome.spec_support.runtime import math
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def section_total_weight(sections: list[SectionEntry]) -> float:
|
|
23
|
+
"""Sum of section weights; the denominator of the pillar-level weighted average."""
|
|
24
|
+
return sum(section_weight for section_weight, _ in sections)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def section_weighted_score(sections: list[SectionEntry]) -> float:
|
|
28
|
+
"""Sum of (section_weight × score) for all sections; the numerator of the pillar-level weighted average."""
|
|
29
|
+
return sum(section_weight * score for section_weight, score in sections)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def overall_score(assessor: AssessorScore) -> float:
|
|
33
|
+
"""Two-level weighted average score for a single assessor.
|
|
34
|
+
|
|
35
|
+
First computes a weighted average score within each pillar
|
|
36
|
+
(``section_weighted_score / section_total_weight``), then weights those
|
|
37
|
+
pillar scores by their pillar weights. Pillars with no sections or a zero
|
|
38
|
+
total section weight are skipped.
|
|
39
|
+
|
|
40
|
+
Will raise ZeroDivisionError if the assessor has no scored subsections or all pillar
|
|
41
|
+
weights sum to zero. Should not happen in practice due to validation of the client's AssessorScore instance.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
total_pillar_weight = sum(pillar_weight for pillar_weight, _ in assessor.scores)
|
|
45
|
+
|
|
46
|
+
pillar_total = sum(
|
|
47
|
+
pillar_weight * section_weighted_score(sections) / section_total_weight(sections)
|
|
48
|
+
for pillar_weight, sections in assessor.scores
|
|
49
|
+
if sections and section_total_weight(sections)
|
|
50
|
+
)
|
|
51
|
+
return pillar_total / total_pillar_weight
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def n_score_1(assessor: AssessorScore) -> int:
|
|
55
|
+
"""Count of subsections where this assessor gave the minimum score of 1.0."""
|
|
56
|
+
return sum(1 for _, sections in assessor.scores for _, score in sections if score == 1.0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def total_n_score_1(assessors: list[AssessorScore]) -> int:
|
|
60
|
+
"""Sum of score-1 subsection counts across all assessors; the input to penalty interpolation."""
|
|
61
|
+
return sum(n_score_1(a) for a in assessors)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def weighted_average(assessors: list[AssessorScore]) -> float:
|
|
65
|
+
"""Arithmetic mean of overall scores across all assessors that produced a valid score."""
|
|
66
|
+
valid_count = sum(1 for a in assessors if overall_score(a))
|
|
67
|
+
valid_sum = sum(overall_score(a) for a in assessors if overall_score(a))
|
|
68
|
+
return valid_sum / valid_count
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def map_crr(avg_score: float, crr_mapping: CRRMapping) -> float:
|
|
72
|
+
"""Interpolate the base CRR% from the aggregate assessor score using the CRR mapping table."""
|
|
73
|
+
return math.linear_interp(
|
|
74
|
+
[avg_score],
|
|
75
|
+
[score for score, _ in crr_mapping.table],
|
|
76
|
+
[crr for _, crr in crr_mapping.table],
|
|
77
|
+
)[0]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def calculate_penalty(n_score1: int, penalty_mapping: PenaltyMapping) -> float:
|
|
81
|
+
"""Interpolate the penalty in percentage points from the count of score-1 subsections."""
|
|
82
|
+
return math.linear_interp(
|
|
83
|
+
[float(n_score1)],
|
|
84
|
+
[n for n, _ in penalty_mapping.table],
|
|
85
|
+
[penalty for _, penalty in penalty_mapping.table],
|
|
86
|
+
)[0]
|
|
@@ -35,6 +35,12 @@ src/axis_synome/spec/crypto_lending/formulas/lif.py
|
|
|
35
35
|
src/axis_synome/spec/risk_capital/__init__.py
|
|
36
36
|
src/axis_synome/spec/risk_capital/formulas/__init__.py
|
|
37
37
|
src/axis_synome/spec/risk_capital/formulas/required_risk_capital.py
|
|
38
|
+
src/axis_synome/spec/suraf/README.md
|
|
39
|
+
src/axis_synome/spec/suraf/entities/assessor_score.py
|
|
40
|
+
src/axis_synome/spec/suraf/entities/assets.py
|
|
41
|
+
src/axis_synome/spec/suraf/entities/mappings.py
|
|
42
|
+
src/axis_synome/spec/suraf/formulas/crr.py
|
|
43
|
+
src/axis_synome/spec/suraf/formulas/scoring.py
|
|
38
44
|
src/axis_synome/spec_support/__init__.py
|
|
39
45
|
src/axis_synome/spec_support/evm_address.py
|
|
40
46
|
src/axis_synome/spec_support/validated_dataclass.py
|
|
@@ -67,4 +73,19 @@ tests/axis_synome/spec_support/runtime/__init__.py
|
|
|
67
73
|
tests/axis_synome/spec_support/runtime/test_base.py
|
|
68
74
|
tests/axis_synome/spec_support/runtime/test_math.py
|
|
69
75
|
tests/axis_synome/spec_validator/test_checker.py
|
|
70
|
-
tests/axis_synome/spec_validator/test_flake8_plugin.py
|
|
76
|
+
tests/axis_synome/spec_validator/test_flake8_plugin.py
|
|
77
|
+
tests/axis_synome/suraf/__init__.py
|
|
78
|
+
tests/axis_synome/suraf/entities/__init__.py
|
|
79
|
+
tests/axis_synome/suraf/entities/test_assessor_score.py
|
|
80
|
+
tests/axis_synome/suraf/formulas/__init__.py
|
|
81
|
+
tests/axis_synome/suraf/formulas/test_crr.py
|
|
82
|
+
tests/axis_synome/suraf/formulas/test_scoring.py
|
|
83
|
+
tests/axis_synome/suraf/static/aave_ausdc/v1/crr_mapping.csv
|
|
84
|
+
tests/axis_synome/suraf/static/aave_ausdc/v1/penalty.csv
|
|
85
|
+
tests/axis_synome/suraf/static/aave_ausdc/v1/weights.csv
|
|
86
|
+
tests/axis_synome/suraf/static/aave_ausdc/v1/scorecards/Assessor_1_scores.csv
|
|
87
|
+
tests/axis_synome/suraf/static/aave_ausdc/v1/scorecards/Assessor_2_scores.csv
|
|
88
|
+
tests/axis_synome/suraf/static/aave_ausdc/v1/scorecards/Assessor_3_scores.csv
|
|
89
|
+
tests/axis_synome/suraf/suraf_client/__init__.py
|
|
90
|
+
tests/axis_synome/suraf/suraf_client/suraf_client.py
|
|
91
|
+
tests/axis_synome/suraf/suraf_client/test_suraf_client.py
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Tests for AssessorScore entity validation."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
from axis_synome.spec.suraf.entities.assessor_score import AssessorScore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestAssessorScoreValidation:
|
|
10
|
+
def test_boundary_scores_accepted(self):
|
|
11
|
+
a = AssessorScore(scores=[(10, [(5, 1), (5, 5)])])
|
|
12
|
+
assert len(a.scores) == 1
|
|
13
|
+
assert len(a.scores[0][1]) == 2
|
|
14
|
+
|
|
15
|
+
def test_score_below_min_rejected(self):
|
|
16
|
+
with pytest.raises(ValidationError):
|
|
17
|
+
AssessorScore(scores=[(10, [(5, 0)])])
|
|
18
|
+
|
|
19
|
+
def test_score_above_max_rejected(self):
|
|
20
|
+
with pytest.raises(ValidationError):
|
|
21
|
+
AssessorScore(scores=[(10, [(5, 6)])])
|
|
22
|
+
|
|
23
|
+
def test_empty_scores_list_accepted(self):
|
|
24
|
+
a = AssessorScore(scores=[])
|
|
25
|
+
assert a.scores == []
|
|
26
|
+
|
|
27
|
+
def test_empty_sections_in_pillar_accepted(self):
|
|
28
|
+
a = AssessorScore(scores=[(10, [])])
|
|
29
|
+
assert a.scores[0][1] == []
|
|
30
|
+
|
|
31
|
+
def test_large_absolute_weights_accepted(self):
|
|
32
|
+
a = AssessorScore(scores=[(40, [(8, 3), (12, 4)])])
|
|
33
|
+
assert len(a.scores) == 1
|
|
File without changes
|