|
| 1 | +import pytest |
| 2 | +import pandas as pd |
| 3 | +import numpy as np |
| 4 | + |
| 5 | +import okama as ok |
| 6 | +from tests.asset_list.conftest import _FakeAsset, _FakeCurrencyAsset |
| 7 | + |
| 8 | + |
| 9 | +@pytest.fixture() |
| 10 | +def _inflation_env(mocker): |
| 11 | + """ |
| 12 | + Prepare a single-asset environment with deterministic monthly inflation. |
| 13 | +
|
| 14 | + Asset A.US has constant monthly return r_a. Inflation is constant monthly r_i. |
| 15 | + We patch ListMaker assets and macro.Inflation to be deterministic and offline. |
| 16 | + """ |
| 17 | + idx = pd.period_range("2020-01", periods=24, freq="M") |
| 18 | + r_a = 0.01 # 1% per month asset |
| 19 | + r_i = 0.002 # 0.2% per month inflation |
| 20 | + |
| 21 | + asset_ror = pd.Series(r_a, index=idx, name="A.US") |
| 22 | + infl_monthly = pd.Series(r_i, index=idx.to_timestamp(how="end"), name="USD.INFL") |
| 23 | + |
| 24 | + # Patch assets |
| 25 | + fake_assets = {"A.US": _FakeAsset("A.US", asset_ror)} |
| 26 | + mocker.patch("okama.common.make_asset_list.ListMaker._get_asset_obj_dict", return_value=fake_assets) |
| 27 | + mocker.patch("okama.common.make_asset_list.asset.Asset", side_effect=_FakeCurrencyAsset) |
| 28 | + |
| 29 | + # Patch Inflation so that AssetList(inflation=True) picks our values |
| 30 | + class _FakeInflation: |
| 31 | + def __init__(self, symbol: str, first_date=None, last_date=None): |
| 32 | + self.symbol = symbol |
| 33 | + # first/last dates are taken from the monthly series index |
| 34 | + self.first_date = infl_monthly.index[0].to_period("M").to_timestamp() |
| 35 | + self.last_date = infl_monthly.index[-1].to_period("M").to_timestamp() |
| 36 | + # ListMaker expects PeriodIndex in _add_inflation path |
| 37 | + self.values_monthly = infl_monthly.to_period("M") |
| 38 | + |
| 39 | + mocker.patch("okama.common.make_asset_list.macro.Inflation", side_effect=_FakeInflation) |
| 40 | + |
| 41 | + return { |
| 42 | + "index": idx, |
| 43 | + "asset_ror": asset_ror, |
| 44 | + "infl_periodic": infl_monthly.to_period("M"), |
| 45 | + "r_a": r_a, |
| 46 | + "r_i": r_i, |
| 47 | + } |
| 48 | + |
| 49 | + |
| 50 | +def test_inflation_basic_pipeline_and_properties(_inflation_env): |
| 51 | + al = ok.AssetList(["A.US"], ccy="USD", inflation=True) |
| 52 | + |
| 53 | + # Inflation attributes should exist and align with our patched series |
| 54 | + assert hasattr(al, "inflation") |
| 55 | + assert hasattr(al, "inflation_ts") |
| 56 | + assert isinstance(al.inflation_ts, pd.Series) |
| 57 | + # Index should be PeriodIndex with monthly frequency and match assets_ror index (inner join applied) |
| 58 | + assert isinstance(al.inflation_ts.index, pd.PeriodIndex) |
| 59 | + assert list(al.inflation_ts.index) == list(al.assets_ror.index) |
| 60 | + |
| 61 | + # Name of inflation series should be the inflation symbol |
| 62 | + assert isinstance(al.inflation, str) and al.inflation.endswith(".INFL") |
| 63 | + assert al.inflation_ts.name == al.inflation |
| 64 | + |
| 65 | + # Mean annualized inflation from monthly 0.2% should be close to 12 * 0.002 for small rates |
| 66 | + # (Exact CAGR is tested below; here only sanity check for arithmetic mean annualization in tests) |
| 67 | + mean_annual = float(al.inflation_ts.values.mean() * 12) |
| 68 | + assert pytest.approx(mean_annual, rel=1e-12) == 0.002 * 12 |
| 69 | + |
| 70 | + |
| 71 | +@pytest.mark.parametrize( |
| 72 | + "compute_metric", |
| 73 | + [ |
| 74 | + "real_mean_return", # property: annualized arithmetic mean adjusted by arithmetic mean inflation |
| 75 | + "real_cagr", # method: get_cagr(real=True) full period |
| 76 | + ], |
| 77 | +) |
| 78 | +def test_real_metrics_with_inflation_parametrized(_inflation_env, compute_metric): |
| 79 | + al = ok.AssetList(["A.US"], ccy="USD", inflation=True) |
| 80 | + |
| 81 | + # Helper to compute expected real mean return using arithmetic means (as in tests elsewhere) |
| 82 | + def expected_real_mean_return(): |
| 83 | + # Asset annualized arithmetic mean from AssetList |
| 84 | + mean_asset_annual = float(al.mean_return["A.US"]) # already annualized in implementation |
| 85 | + # Inflation annualized arithmetic mean (monthly -> annual by *12) |
| 86 | + infl_mean_annual = float(al.inflation_ts.values.mean() * 12) |
| 87 | + return (1.0 + mean_asset_annual) / (1.0 + infl_mean_annual) - 1.0 |
| 88 | + |
| 89 | + # Helper to compute expected real CAGR over the full period |
| 90 | + def expected_real_cagr_full(): |
| 91 | + ror = al.assets_ror["A.US"] |
| 92 | + n = len(ror) |
| 93 | + wi_last = float((1.0 + ror).prod()) |
| 94 | + # Annualize to 12 months per year |
| 95 | + cagr_asset = wi_last ** (12.0 / n) - 1.0 |
| 96 | + |
| 97 | + infl = al.inflation_ts |
| 98 | + wi_infl_last = float((1.0 + infl).prod()) |
| 99 | + cagr_infl = wi_infl_last ** (12.0 / n) - 1.0 |
| 100 | + return (1.0 + cagr_asset) / (1.0 + cagr_infl) - 1.0 |
| 101 | + |
| 102 | + if compute_metric == "real_mean_return": |
| 103 | + got = float(al.real_mean_return["A.US"]) # property should exist for inflation=True case |
| 104 | + exp = expected_real_mean_return() |
| 105 | + assert pytest.approx(got, rel=1e-12) == exp |
| 106 | + |
| 107 | + elif compute_metric == "real_cagr": |
| 108 | + got = float(al.get_cagr(real=True)["A.US"]) # full-period real CAGR |
| 109 | + exp = expected_real_cagr_full() |
| 110 | + assert pytest.approx(got, rel=1e-12) == exp |
| 111 | + |
| 112 | + else: |
| 113 | + raise AssertionError("Unknown metric param") |
0 commit comments