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

1"""Linear-algebra and probability-plotting helpers. 

2 

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 

8 

9import numpy as np 

10 

11 

12def ppoints(n: int, a: float | None = None) -> np.ndarray: 

13 """Generate probability points for Q-Q plots and distribution fitting. 

14 

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. 

18 

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). 

23 

24 Returns: 

25 Array of n equidistant probability points in (0, 1). 

26 

27 Raises: 

28 ValueError: If a is not in [0, 1]. 

29 

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) 

45 

46 

47def robust_covariance_inverse(V: np.ndarray) -> np.ndarray: 

48 r"""Compute inverse of a constant-correlation covariance matrix. 

49 

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). 

54 

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 }$ 

58 

59 Args: 

60 V: Covariance matrix with constant off-diagonal correlations. 

61 Shape (n, n). 

62 

63 Returns: 

64 Inverse of the covariance matrix. Shape (n, n). 

65 

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 

85 

86 

87def minimum_variance_weights_for_correlated_assets(V: np.ndarray) -> np.ndarray: 

88 """Compute weights of the minimum variance portfolio for correlated assets. 

89 

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. 

93 

94 Args: 

95 V: Covariance matrix of asset returns. Shape (n, n). 

96 

97 Returns: 

98 Portfolio weights that minimize variance. Shape (n,). 

99 Weights sum to 1. 

100 

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