Coverage for src/stable_yield_lab/risk_scoring.py: 100%
23 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-04 20:38 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-04 20:38 +0000
1from __future__ import annotations
3"""Heuristic risk scoring for stablecoin yield pools."""
5from dataclasses import replace
6from typing import Mapping, TYPE_CHECKING
8if TYPE_CHECKING: # pragma: no cover - imported only for type checking
9 from stable_yield_lab.core import Pool
11# Simple reputation mapping per chain. Values range from 0 (unknown) to 1 (blue chip).
12CHAIN_REPUTATION: Mapping[str, float] = {
13 "Ethereum": 1.0,
14 "Arbitrum": 0.9,
15 "Polygon": 0.7,
16 "BSC": 0.6,
17 "Tron": 0.4,
18}
21def calculate_risk_score(chain_rep: float, audits: int, yield_volatility: float) -> float:
22 """Combine factors into a normalized risk score in the range [1, 3].
24 Parameters
25 ----------
26 chain_rep:
27 Reputation of the underlying chain, ``0`` (unknown) to ``1`` (established).
28 audits:
29 Number of protocol security audits. Values above ``5`` are capped.
30 yield_volatility:
31 Normalized measure of historical yield volatility (``0`` stable, ``1`` erratic).
32 """
34 # Clamp inputs to expected ranges
35 chain_rep = max(0.0, min(chain_rep, 1.0))
36 audits = max(0, min(audits, 5))
37 yield_volatility = max(0.0, min(yield_volatility, 1.0))
39 chain_component = 1.0 - chain_rep # higher reputation lowers risk
40 audit_component = 1.0 - audits / 5.0 # more audits lower risk
41 vol_component = yield_volatility # higher volatility increases risk
43 raw = (chain_component + audit_component + vol_component) / 3.0
44 return 1.0 + 2.0 * raw
47def score_pool(
48 pool: "Pool",
49 *,
50 chain_reputation: Mapping[str, float] | None = None,
51 protocol_audits: Mapping[str, int] | None = None,
52 yield_volatility: Mapping[str, float] | None = None,
53) -> "Pool":
54 """Return a new :class:`~stable_yield_lab.Pool` with an updated ``risk_score``.
56 Missing mapping entries default to neutral values (``0.5`` reputation,
57 ``0`` audits and ``0`` volatility).
58 """
60 chain_rep_map = chain_reputation or CHAIN_REPUTATION
61 audits_map = protocol_audits or {}
62 vol_map = yield_volatility or {}
64 chain_rep = chain_rep_map.get(pool.chain, 0.5)
65 audits = audits_map.get(pool.name, 0)
66 vol = vol_map.get(pool.name, 0.0)
68 score = calculate_risk_score(chain_rep, audits, vol)
69 return replace(pool, risk_score=score)