|
| 1 | +""" |
| 2 | +Value at Risk (VaR) via historical simulation. |
| 3 | +
|
| 4 | +References: |
| 5 | +- https://en.wikipedia.org/wiki/Value_at_risk |
| 6 | +- https://www.investopedia.com/terms/v/var.asp |
| 7 | +
|
| 8 | +Value at Risk measures the maximum loss a portfolio can suffer over a given |
| 9 | +period at a chosen confidence level. Historical simulation is a |
| 10 | +non-parametric method: it reuses the observed returns and reads the quantile |
| 11 | +from the empirical distribution, so no assumption is made about the shape of |
| 12 | +the loss distribution. The result is the negative of the corresponding return |
| 13 | +quantile, i.e. a positive loss magnitude when the tail of the distribution |
| 14 | +contains losses. |
| 15 | +""" |
| 16 | + |
| 17 | +from collections.abc import Sequence |
| 18 | +from math import isfinite |
| 19 | + |
| 20 | + |
| 21 | +def _linear_interpolated_quantile( |
| 22 | + sorted_values: Sequence[float], quantile: float |
| 23 | +) -> float: |
| 24 | + """ |
| 25 | + Linear interpolation between the closest ranks (NumPy default, type 7). |
| 26 | +
|
| 27 | + >>> _linear_interpolated_quantile([-10.0, -5.0, -2.0, 1.0, 4.0], 0.05) |
| 28 | + -9.0 |
| 29 | + >>> _linear_interpolated_quantile([1.0, 2.0, 3.0], 1.0) |
| 30 | + 3.0 |
| 31 | + """ |
| 32 | + position = (len(sorted_values) - 1) * quantile |
| 33 | + lower_index = int(position) |
| 34 | + fraction = position - lower_index |
| 35 | + if lower_index == len(sorted_values) - 1: |
| 36 | + return sorted_values[-1] |
| 37 | + return sorted_values[lower_index] * (1 - fraction) + ( |
| 38 | + sorted_values[lower_index + 1] * fraction |
| 39 | + ) |
| 40 | + |
| 41 | + |
| 42 | +def value_at_risk(returns: Sequence[float], confidence_level: float = 0.95) -> float: |
| 43 | + """ |
| 44 | + Calculate the historical-simulation Value at Risk of a portfolio. |
| 45 | +
|
| 46 | + The confidence level is the probability that the loss will not exceed the |
| 47 | + returned value. The default of 0.95 means that 95% of the observed returns |
| 48 | + are better (higher) than the VaR threshold, and the remaining 5% are worse. |
| 49 | +
|
| 50 | + Examples: |
| 51 | + >>> value_at_risk([-10, -5, -2, 1, 4], 0.95) |
| 52 | + 9.0 |
| 53 | + >>> value_at_risk([-2, -1, 0, 1, 2, 3], 0.90) |
| 54 | + 1.5 |
| 55 | + >>> value_at_risk([5, 10, 15], 0.75) |
| 56 | + -7.5 |
| 57 | + >>> value_at_risk([], 0.95) |
| 58 | + Traceback (most recent call last): |
| 59 | + ... |
| 60 | + ValueError: returns must not be empty |
| 61 | + >>> value_at_risk([-1, 0, 1], 1.0) |
| 62 | + Traceback (most recent call last): |
| 63 | + ... |
| 64 | + ValueError: confidence_level must be strictly between 0 and 1 |
| 65 | + >>> value_at_risk([-1, float("nan"), 1], 0.95) |
| 66 | + Traceback (most recent call last): |
| 67 | + ... |
| 68 | + ValueError: returns must contain only finite numbers |
| 69 | +
|
| 70 | + Time complexity: O(n log n), where n = len(returns), for sorting. |
| 71 | + Space complexity: O(n) for the sorted copy. |
| 72 | + """ |
| 73 | + if not returns: |
| 74 | + raise ValueError("returns must not be empty") |
| 75 | + if not all(isfinite(value) for value in returns): |
| 76 | + raise ValueError("returns must contain only finite numbers") |
| 77 | + if not 0 < confidence_level < 1: |
| 78 | + raise ValueError("confidence_level must be strictly between 0 and 1") |
| 79 | + |
| 80 | + sorted_returns = sorted(returns) |
| 81 | + threshold = _linear_interpolated_quantile(sorted_returns, 1 - confidence_level) |
| 82 | + return -threshold |
| 83 | + |
| 84 | + |
| 85 | +if __name__ == "__main__": |
| 86 | + import doctest |
| 87 | + |
| 88 | + doctest.testmod() |
0 commit comments