pyreto 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.
pyreto/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Pyreto: Tail risk management library."""
2
+
3
+ from .distributions import fit
4
+
5
+ __version__ = "0.2.0"
6
+ __all__ = ["fit"]
@@ -0,0 +1,8 @@
1
+ """Distributions module."""
2
+
3
+ from .fit import fit
4
+ from .student_t import StudentT
5
+ from .alpha_stable import AlphaStable
6
+ from .normal_inverse_gaussian import NormalInverseGaussian
7
+
8
+ __all__ = ["fit", "StudentT", "AlphaStable", "NormalInverseGaussian"]
@@ -0,0 +1,103 @@
1
+ """Alpha-Stable distribution."""
2
+
3
+ import numpy as np
4
+ from scipy.optimize import minimize
5
+ from scipy.integrate import quad
6
+ from scipy.optimize import brentq
7
+ from scipy.stats import norm
8
+ from .base import DistributionFitter
9
+
10
+
11
+ class AlphaStable(DistributionFitter):
12
+ """Alpha-Stable distribution."""
13
+
14
+ def _char_func(self, t, alpha, beta, c, mu):
15
+ if alpha == 1:
16
+ return np.exp(1j * mu * t - c * np.abs(t) * (1 + 1j * beta * np.sign(t) * np.log(np.abs(t))))
17
+ else:
18
+ return np.exp(1j * mu * t - c**alpha * np.abs(t)**alpha *
19
+ (1 - 1j * beta * np.sign(t) * np.tan(np.pi * alpha / 2)))
20
+
21
+ def _pdf(self, x, alpha, beta, c, mu):
22
+ if alpha == 2.0:
23
+ return norm.pdf(x, loc=mu, scale=c)
24
+
25
+ t = np.linspace(-50, 50, 1024)
26
+ x = np.atleast_1d(x)
27
+ results = np.zeros_like(x)
28
+
29
+ for i, xi in enumerate(x):
30
+ cf = self._char_func(t, alpha, beta, c, mu - xi)
31
+ dt = t[1] - t[0]
32
+ cf_sym = np.concatenate([cf, np.conj(cf[-1:0:-1])])
33
+ pdf_component = np.fft.ifft(cf_sym).real[:len(t)] * len(t) / (2 * np.pi)
34
+ results[i] = np.abs(pdf_component[0])
35
+
36
+ return np.maximum(results, 1e-10)
37
+
38
+ def fit(self) -> dict:
39
+ """Fit using MLE. Returns dict with alpha, beta, c, mu."""
40
+ def neg_loglik(params):
41
+ alpha, beta, c, mu = params
42
+ if not (0.1 < alpha <= 2.0) or not (-1 <= beta <= 1) or c <= 0:
43
+ return 1e10
44
+ try:
45
+ pdf_vals = self._pdf(self.returns, alpha, beta, c, mu)
46
+ if np.any(pdf_vals <= 0):
47
+ return 1e10
48
+ return -np.sum(np.log(pdf_vals))
49
+ except:
50
+ return 1e10
51
+
52
+ mu0 = np.mean(self.returns)
53
+ c0 = np.std(self.returns) / np.sqrt(2)
54
+ alpha0 = 1.8
55
+ beta0 = 0.0
56
+ x0 = np.array([alpha0, beta0, c0, mu0])
57
+ bounds = [(0.1, 2.0), (-0.999, 0.999), (1e-6, 10.0), (None, None)]
58
+
59
+ res = minimize(neg_loglik, x0, method='Nelder-Mead',
60
+ options={'maxiter': 500, 'xatol': 1e-6, 'fatol': 1e-6})
61
+
62
+ alpha, beta, c, mu = res.x
63
+ self.params = {'alpha': alpha, 'beta': beta, 'c': c, 'mu': mu}
64
+ return self.params
65
+
66
+ def pdf(self, x: np.ndarray) -> np.ndarray:
67
+ if not self.params:
68
+ self.fit()
69
+ params = self.params
70
+ return self._pdf(x, params['alpha'], params['beta'], params['c'], params['mu'])
71
+
72
+ def cdf(self, x: np.ndarray) -> np.ndarray:
73
+ if not self.params:
74
+ self.fit()
75
+ x = np.atleast_1d(x)
76
+ results = np.zeros_like(x)
77
+ for i, xi in enumerate(x):
78
+ results[i], _ = quad(lambda xx: self.pdf(np.array([xx]))[0], -np.inf, xi)
79
+ return results
80
+
81
+ def ppf(self, q: np.ndarray) -> np.ndarray:
82
+ if not self.params:
83
+ self.fit()
84
+ q = np.atleast_1d(q)
85
+ results = np.zeros_like(q)
86
+ for i, qi in enumerate(q):
87
+ def f(x):
88
+ return self.cdf(np.array([x]))[0] - qi
89
+ results[i] = brentq(f, -10, 10)
90
+ return results
91
+
92
+ def es(self, alpha: float) -> float:
93
+ if not (0 < alpha < 1):
94
+ raise ValueError("Alpha must be between 0 and 1")
95
+ if not self.params:
96
+ self.fit()
97
+ var = self.var(alpha)
98
+
99
+ def integrand(x):
100
+ return x * self.pdf(np.array([x]))[0]
101
+
102
+ es_val, _ = quad(integrand, -np.inf, var)
103
+ return es_val / alpha
@@ -0,0 +1,48 @@
1
+ """Base class for distribution fitters."""
2
+
3
+ import numpy as np
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class DistributionFitter(ABC):
8
+ """Abstract base class for distribution fitting."""
9
+
10
+ def __init__(self, returns: np.ndarray):
11
+ returns = np.array(returns, dtype=np.float64).flatten()
12
+ if not np.isfinite(returns).all():
13
+ raise ValueError("Returns must be finite (no NaN or Inf)")
14
+ if len(returns) == 0:
15
+ raise ValueError("Returns cannot be empty")
16
+ self.returns = returns
17
+ self.params = {}
18
+
19
+ @abstractmethod
20
+ def fit(self) -> dict:
21
+ """Fit distribution and return parameters."""
22
+ pass
23
+
24
+ @abstractmethod
25
+ def pdf(self, x: np.ndarray) -> np.ndarray:
26
+ """Probability density function."""
27
+ pass
28
+
29
+ @abstractmethod
30
+ def cdf(self, x: np.ndarray) -> np.ndarray:
31
+ """Cumulative distribution function."""
32
+ pass
33
+
34
+ @abstractmethod
35
+ def ppf(self, q: np.ndarray) -> np.ndarray:
36
+ """Percent point function (inverse CDF)."""
37
+ pass
38
+
39
+ @abstractmethod
40
+ def es(self, alpha: float) -> float:
41
+ """Expected Shortfall at significance level alpha."""
42
+ pass
43
+
44
+ def var(self, alpha: float) -> float:
45
+ """Value at Risk (quantile) at significance level alpha."""
46
+ if not (0 < alpha < 1):
47
+ raise ValueError("Alpha must be between 0 and 1")
48
+ return self.ppf(np.array([alpha]))[0]
@@ -0,0 +1,86 @@
1
+ """Main fit function."""
2
+
3
+ import numpy as np
4
+ from .student_t import StudentT
5
+ from .alpha_stable import AlphaStable
6
+ from .normal_inverse_gaussian import NormalInverseGaussian
7
+
8
+
9
+ MODEL_MAP = {
10
+ 'student_t': StudentT,
11
+ 'alpha_stable': AlphaStable,
12
+ 'nig': NormalInverseGaussian,
13
+ 'normal_inverse_gaussian': NormalInverseGaussian
14
+ }
15
+
16
+
17
+ def load_returns(data, column=None):
18
+ """Load and validate returns from various input types."""
19
+ try:
20
+ import polars as pl
21
+ except ImportError:
22
+ raise ImportError("Polars is required for DataFrame/CSV support. Install with: pip install polars")
23
+
24
+ if isinstance(data, str):
25
+ df = pl.read_csv(data)
26
+ if column is None:
27
+ numeric_cols = [col for col, dtype in df.schema.items()
28
+ if dtype in [pl.Float64, pl.Float32, pl.Int64, pl.Int32]]
29
+ if len(numeric_cols) == 1:
30
+ column = numeric_cols[0]
31
+ else:
32
+ raise ValueError("Multiple numeric columns found. Specify column parameter.")
33
+ returns = df[column].to_numpy()
34
+ elif hasattr(data, 'to_numpy'):
35
+ if column is None:
36
+ numeric_cols = [col for col, dtype in data.schema.items()
37
+ if dtype in [pl.Float64, pl.Float32, pl.Int64, pl.Int32]]
38
+ if len(numeric_cols) == 1:
39
+ column = numeric_cols[0]
40
+ else:
41
+ raise ValueError("Multiple numeric columns found. Specify column parameter.")
42
+ returns = data[column].to_numpy()
43
+ elif isinstance(data, np.ndarray):
44
+ returns = data.flatten()
45
+ elif isinstance(data, list):
46
+ returns = np.array(data)
47
+ else:
48
+ raise TypeError(f"Unsupported data type: {type(data)}")
49
+
50
+ returns = returns.astype(np.float64)
51
+ if not np.isfinite(returns).all():
52
+ raise ValueError("Returns contain non-finite values (NaN or Inf)")
53
+ if len(returns) == 0:
54
+ raise ValueError("Returns cannot be empty")
55
+
56
+ return returns
57
+
58
+
59
+ def fit(data, column=None, model='student_t'):
60
+ """Fit a single distribution model to returns data.
61
+
62
+ Parameters
63
+ ----------
64
+ data : str, DataFrame, np.ndarray, or list
65
+ Returns data
66
+ column : str, optional
67
+ Column name if data is DataFrame or CSV
68
+ model : str
69
+ Model to fit: 'student_t', 'alpha_stable', 'nig'
70
+
71
+ Returns
72
+ -------
73
+ DistributionFitter
74
+ Fitted distribution object
75
+ """
76
+ returns = load_returns(data, column)
77
+ model_name = model.lower()
78
+
79
+ if model_name not in MODEL_MAP:
80
+ raise ValueError(f"Unknown model: {model}. Available: {list(MODEL_MAP.keys())}")
81
+
82
+ ModelClass = MODEL_MAP[model_name]
83
+ instance = ModelClass(returns)
84
+ instance.fit()
85
+
86
+ return instance
@@ -0,0 +1,83 @@
1
+ """Normal-Inverse Gaussian distribution."""
2
+
3
+ import numpy as np
4
+ from scipy.optimize import minimize
5
+ from scipy.special import kv as bessel_k
6
+ from scipy.integrate import quad
7
+ from scipy.optimize import brentq
8
+ from .base import DistributionFitter
9
+
10
+
11
+ class NormalInverseGaussian(DistributionFitter):
12
+ """Normal-Inverse Gaussian distribution."""
13
+
14
+ def _pdf(self, x, alpha, beta, delta, mu):
15
+ gamma = np.sqrt(alpha**2 - beta**2)
16
+ z = (x - mu) / delta
17
+ factor = alpha * delta * np.sqrt(1 + z**2)
18
+ numerator = alpha * delta * bessel_k(1, factor)
19
+ denominator = np.pi * np.sqrt(1 + z**2)
20
+ return (numerator / denominator) * np.exp(delta * gamma + beta * (x - mu))
21
+
22
+ def fit(self) -> dict:
23
+ """Fit using MLE. Returns dict with alpha, beta, delta, mu."""
24
+ def neg_loglik(params):
25
+ alpha, beta, delta, mu = params
26
+ if alpha <= 0 or delta <= 0 or abs(beta) >= alpha:
27
+ return 1e10
28
+ try:
29
+ pdf_vals = self._pdf(self.returns, alpha, beta, delta, mu)
30
+ return -np.sum(np.log(pdf_vals))
31
+ except:
32
+ return 1e10
33
+
34
+ mu0 = np.mean(self.returns)
35
+ delta0 = np.std(self.returns) * 0.8
36
+ alpha0 = 2.0
37
+ beta0 = 0.0
38
+ x0 = np.array([alpha0, beta0, delta0, mu0])
39
+ bounds = [(1e-4, 50), (-49, 49), (1e-4, 50), (None, None)]
40
+
41
+ res = minimize(neg_loglik, x0, method='SLSQP', bounds=bounds,
42
+ options={'ftol': 1e-8, 'maxiter': 1000})
43
+
44
+ alpha, beta, delta, mu = res.x
45
+ self.params = {'alpha': alpha, 'beta': beta, 'delta': delta, 'mu': mu,
46
+ 'gamma': np.sqrt(alpha**2 - beta**2)}
47
+ return self.params
48
+
49
+ def pdf(self, x: np.ndarray) -> np.ndarray:
50
+ if not self.params:
51
+ self.fit()
52
+ return self._pdf(x, **{k: self.params[k] for k in ['alpha', 'beta', 'delta', 'mu']})
53
+
54
+ def cdf(self, x: np.ndarray) -> np.ndarray:
55
+ if not self.params:
56
+ self.fit()
57
+ results = np.zeros_like(x)
58
+ for i, xi in enumerate(x):
59
+ results[i], _ = quad(lambda xx: self.pdf(np.array([xx]))[0], -np.inf, xi)
60
+ return results
61
+
62
+ def ppf(self, q: np.ndarray) -> np.ndarray:
63
+ if not self.params:
64
+ self.fit()
65
+ results = np.zeros_like(q)
66
+ for i, qi in enumerate(q):
67
+ def f(x):
68
+ return self.cdf(np.array([x]))[0] - qi
69
+ results[i] = brentq(f, -10, 10)
70
+ return results
71
+
72
+ def es(self, alpha: float) -> float:
73
+ if not (0 < alpha < 1):
74
+ raise ValueError("Alpha must be between 0 and 1")
75
+ if not self.params:
76
+ self.fit()
77
+ var = self.var(alpha)
78
+
79
+ def integrand(x):
80
+ return x * self.pdf(np.array([x]))[0]
81
+
82
+ es_val, _ = quad(integrand, -np.inf, var)
83
+ return es_val / alpha
@@ -0,0 +1,44 @@
1
+ """Student's T distribution."""
2
+
3
+ import numpy as np
4
+ from scipy.stats import t as student_t
5
+ from scipy.integrate import quad
6
+ from .base import DistributionFitter
7
+
8
+
9
+ class StudentT(DistributionFitter):
10
+ """Student's T distribution."""
11
+
12
+ def fit(self) -> dict:
13
+ """Fit using MLE. Returns dict with df, loc, scale."""
14
+ df, loc, scale = student_t.fit(self.returns)
15
+ self.params = {'df': df, 'loc': loc, 'scale': scale}
16
+ return self.params
17
+
18
+ def pdf(self, x: np.ndarray) -> np.ndarray:
19
+ if not self.params:
20
+ self.fit()
21
+ return student_t.pdf(x, **self.params)
22
+
23
+ def cdf(self, x: np.ndarray) -> np.ndarray:
24
+ if not self.params:
25
+ self.fit()
26
+ return student_t.cdf(x, **self.params)
27
+
28
+ def ppf(self, q: np.ndarray) -> np.ndarray:
29
+ if not self.params:
30
+ self.fit()
31
+ return student_t.ppf(q, **self.params)
32
+
33
+ def es(self, alpha: float) -> float:
34
+ if not (0 < alpha < 1):
35
+ raise ValueError("Alpha must be between 0 and 1")
36
+ if not self.params:
37
+ self.fit()
38
+ var = self.var(alpha)
39
+
40
+ def integrand(x):
41
+ return x * self.pdf(np.array([x]))[0]
42
+
43
+ es_val, _ = quad(integrand, -np.inf, var)
44
+ return es_val / alpha
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyreto
3
+ Version: 0.2.0
4
+ Summary: Tail risk management library
5
+ Author-email: Developer <dev@example.com>
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: numpy>=1.20.0
10
+ Requires-Dist: scipy>=1.7.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=6.0; extra == "dev"
13
+
14
+ # pyreto
15
+
16
+ Tail risk management library.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install pyreto
22
+ ```
23
+
24
+ Requires: numpy, scipy
25
+ Optional: polars (for CSV/DataFrame support)
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ import pyreto
31
+
32
+ # Fit Student's T distribution
33
+ model = pyreto.dist.fit('returns.csv', column='daily_return', model='student_t')
34
+
35
+ # Calculate 1% Expected Shortfall
36
+ es_1 = model.es(0.01)
37
+
38
+ # Calculate 5% Value at Risk
39
+ var_5 = model.var(0.05)
40
+
41
+ # Get fitted parameters
42
+ params = model.params
43
+ ```
44
+
45
+ ## Models
46
+
47
+ - `student_t`: Student's T distribution (recommended)
48
+ - `alpha_stable`: Alpha stable distribution (flexible, slower)
49
+ - `nig`: Normal-inverse Gaussian (good middle ground)
50
+
51
+ ## API
52
+
53
+ All models support:
54
+ - `fit()` - Fit parameters
55
+ - `pdf(x)` - Density function
56
+ - `cdf(x)` - Distribution function
57
+ - `ppf(q)` - Quantile function
58
+ - `var(alpha)` - Value at Risk
59
+ - `es(alpha)` - Expected Shortfall
60
+ - `params` - Fitted parameters dict
61
+
62
+ ## Example
63
+
64
+ ```python
65
+ import numpy as np
66
+ import pyreto
67
+
68
+ # Generate heavy-tailed returns
69
+ returns = np.random.standard_t(3, 1000) * 0.01
70
+
71
+ # Fit and calculate tail risk
72
+ model = pyreto.dist.fit(returns, model='student_t')
73
+ print(f"5% VaR: {model.var(0.05):.4f}")
74
+ print(f"5% ES: {model.es(0.05):.4f}")
75
+ ```
76
+
77
+ ## Error Handling
78
+
79
+ The library fails loudly on invalid inputs:
80
+ - Non-finite values (NaN, Inf) throw `ValueError`
81
+ - Invalid alpha values throw `ValueError`
82
+ - Missing parameters throw `ValueError`
@@ -0,0 +1,11 @@
1
+ pyreto/__init__.py,sha256=yfQfScj6LZ9z_4kj9FUhYz5m5msxEcg3KW7ZQN0z44U,117
2
+ pyreto/distributions/__init__.py,sha256=11IhaYFDoth3-b_HbMHT1nG4ZUmMpE6wgz8yLUE3eZI,250
3
+ pyreto/distributions/alpha_stable.py,sha256=f5yktEuCPdHosY7FwaNvbrINec5JK6PQ_EPiwFBt9p8,3622
4
+ pyreto/distributions/base.py,sha256=uBG6ksORlwBs1Tx3aJpDS0qlUp-GP7jhQRMzSLoW-WU,1474
5
+ pyreto/distributions/fit.py,sha256=QAXixS7dAHmR1IMLtRQ_eX7ZkSDGcLyNZi6I6myjzFQ,2795
6
+ pyreto/distributions/normal_inverse_gaussian.py,sha256=0phdT1BFucX58Ps34JfeyWL18m3OuW_dosYjMwmLAog,2964
7
+ pyreto/distributions/student_t.py,sha256=WQdHxVHBn6AJKE3RkwfbaEU_cQug6Neoqg2TdicHIII,1318
8
+ pyreto-0.2.0.dist-info/METADATA,sha256=ujtd_zw9UyLRql6x3caPjgvNVReNduIXztl1JZJbLBw,1732
9
+ pyreto-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ pyreto-0.2.0.dist-info/top_level.txt,sha256=W3ftAjmrhZHXGDjIYS5P1eimmjeytWJ2FsSnViAyAGs,7
11
+ pyreto-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyreto