Coverage for src/jsharpe/sharpe/linalg.py: 100%
21 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-29 13:57 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-29 13:57 +0000
1"""Linear-algebra and probability-plotting helpers.
3This module groups low-level numerical utilities used throughout the
4package: probability plotting positions and constant-correlation
5covariance routines (inversion and minimum-variance weights).
6"""
7# ruff: noqa: N803, N806, TRY003
9import numpy as np
12def ppoints(n: int, a: float | None = None) -> np.ndarray:
13 """Generate probability points for Q-Q plots and distribution fitting.
15 Creates n equidistant points in the interval (0, 1), suitable for
16 probability plotting positions. The boundaries 0 and 1 are excluded.
17 This function mirrors the behavior of R's `ppoints()` function.
19 Args:
20 n: Number of probability points to generate.
21 a: Plotting position parameter in [0, 1]. Defaults to 0.5 if n > 10,
22 otherwise 3/8 (following R's convention).
24 Returns:
25 Array of n equidistant probability points in (0, 1).
27 Raises:
28 ValueError: If a is not in [0, 1].
30 Example:
31 >>> import numpy as np
32 >>> pts = ppoints(5)
33 >>> len(pts)
34 5
35 >>> np.all((pts > 0) & (pts < 1))
36 np.True_
37 >>> ppoints(5, a=0.5)
38 array([0.1, 0.3, 0.5, 0.7, 0.9])
39 """
40 if a is None:
41 a = 0.5 if n > 10 else 3 / 8
42 if not (0 <= a <= 1):
43 raise ValueError(f"the offset should be in [0,1], got {a}")
44 return np.linspace(1 - a, n - a, n) / (n + 1 - 2 * a)
47def robust_covariance_inverse(V: np.ndarray) -> np.ndarray:
48 r"""Compute inverse of a constant-correlation covariance matrix.
50 Uses the Sherman-Morrison formula for efficient computation.
51 Assumes the variance matrix has the form:
52 $V = \rho \sigma \sigma' + (1-\rho) \text{diag}(\sigma^2)$
53 (constant correlations across all pairs).
55 Its inverse is computed as:
56 $V^{-1} = A^{-1} - \dfrac{ A^{-1} \rho \sigma \sigma' A^{-1} }
57 { 1 + \rho \sigma' A^{-1} \sigma }$
59 Args:
60 V: Covariance matrix with constant off-diagonal correlations.
61 Shape (n, n).
63 Returns:
64 Inverse of the covariance matrix. Shape (n, n).
66 Example:
67 >>> import numpy as np
68 >>> # Create a constant-correlation covariance matrix
69 >>> rho = 0.5
70 >>> sigma = np.array([1.0, 2.0, 1.5])
71 >>> C = rho * np.ones((3, 3))
72 >>> np.fill_diagonal(C, 1)
73 >>> V = (C * sigma.reshape(-1, 1)).T * sigma.reshape(-1, 1)
74 >>> V_inv = robust_covariance_inverse(V)
75 >>> np.allclose(V @ V_inv, np.eye(3), atol=1e-10)
76 True
77 """
78 sigma = np.sqrt(np.diag(V))
79 C = (V.T / sigma).T / sigma
80 rho = np.mean(C[np.triu_indices_from(C, 1)])
81 A = np.diag(1 / sigma**2) / (1 - rho)
82 sigma = sigma.reshape(-1, 1)
83 result: np.ndarray = A - (rho * A @ sigma @ sigma.T @ A) / (1 + rho * sigma.T @ A @ sigma)
84 return result
87def minimum_variance_weights_for_correlated_assets(V: np.ndarray) -> np.ndarray:
88 """Compute weights of the minimum variance portfolio for correlated assets.
90 Computes the portfolio weights that minimize portfolio variance subject
91 to the constraint that weights sum to 1. Assumes a constant-correlation
92 covariance structure for efficient computation.
94 Args:
95 V: Covariance matrix of asset returns. Shape (n, n).
97 Returns:
98 Portfolio weights that minimize variance. Shape (n,).
99 Weights sum to 1.
101 Example:
102 >>> import numpy as np
103 >>> # Create a simple covariance matrix
104 >>> V = np.array([[0.04, 0.01], [0.01, 0.09]])
105 >>> w = minimum_variance_weights_for_correlated_assets(V)
106 >>> np.isclose(w.sum(), 1.0)
107 np.True_
108 """
109 ones = np.ones(shape=V.shape[0])
110 S = robust_covariance_inverse(V)
111 w = S @ ones
112 w = w / np.sum(w)
113 return w