privateassets 0.1.0__tar.gz
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.
- privateassets-0.1.0/CHANGELOG.md +15 -0
- privateassets-0.1.0/DATA_README.md +17 -0
- privateassets-0.1.0/LICENSE.txt +21 -0
- privateassets-0.1.0/MANIFEST.in +11 -0
- privateassets-0.1.0/PKG-INFO +76 -0
- privateassets-0.1.0/README.md +56 -0
- privateassets-0.1.0/privateassets/__init__.py +11 -0
- privateassets-0.1.0/privateassets/matf/__init__.py +47 -0
- privateassets-0.1.0/privateassets/matf/_deflator.py +187 -0
- privateassets-0.1.0/privateassets/matf/_panel_mle.py +126 -0
- privateassets-0.1.0/privateassets/matf/_pipeline.py +1887 -0
- privateassets-0.1.0/privateassets/matf/_pme.py +279 -0
- privateassets-0.1.0/privateassets/matf/_rolling_covar.py +70 -0
- privateassets-0.1.0/privateassets/matf/_unsmooth.py +47 -0
- privateassets-0.1.0/privateassets.egg-info/PKG-INFO +76 -0
- privateassets-0.1.0/privateassets.egg-info/SOURCES.txt +19 -0
- privateassets-0.1.0/privateassets.egg-info/dependency_links.txt +1 -0
- privateassets-0.1.0/privateassets.egg-info/requires.txt +8 -0
- privateassets-0.1.0/privateassets.egg-info/top_level.txt +1 -0
- privateassets-0.1.0/pyproject.toml +28 -0
- privateassets-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.1 (2026-06)
|
|
4
|
+
|
|
5
|
+
Initial release of the private-asset analytics package.
|
|
6
|
+
|
|
7
|
+
- `matf`: multi-factor, money-weighted PME. Tradable multi-factor deflator,
|
|
8
|
+
KS-PME / Direct Alpha / Long-Nickels, panel-MLE unsmoothing, rolling EWMA
|
|
9
|
+
covariance, sign-coherent shrinkage betas, and the MATF Direct Alpha pipeline.
|
|
10
|
+
- 14 illustration scripts under `matf/illustrations/` that regenerate the
|
|
11
|
+
precursor figures via `python -m privateassets.matf <id>`.
|
|
12
|
+
- Built on `qis` and `factorlasso`. No dependency on `optimalportfolios`
|
|
13
|
+
(sibling, not child).
|
|
14
|
+
- Flat package layout at the repository root, matching the `optimalportfolios`
|
|
15
|
+
house style.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Data — licensed, not committed
|
|
2
|
+
|
|
3
|
+
**No data files are stored in this repository.** Every input is obtained under
|
|
4
|
+
licence and placed in `data/` locally; `data/` is git-ignored in full. The public
|
|
5
|
+
package must never contain vendor or LP data, nor any derived file from which it
|
|
6
|
+
could be reconstructed.
|
|
7
|
+
|
|
8
|
+
| Source | Used by | Granularity | Publication status |
|
|
9
|
+
|---|---|---|---|
|
|
10
|
+
| **MSCI Private Capital — CFO dataset** | JFQA paper, SAA/CMA | Cohort (strategy × vintage), 1990+ | Aggregated derived results publishable **with MSCI attribution + standard pre-publication review** |
|
|
11
|
+
| **Preqin (BlackRock) — fund-level cash flows** | FAJ paper, selection | Named fund-level | Publication terms **pending** (Boris Hamm / BlackRock) — confirm before use |
|
|
12
|
+
| **Oaktree / Crown LP cash flows** | Precursor draft only | Fund-level | **Internal only — never publishable** |
|
|
13
|
+
| MATF factor levels, rate series | all | — | LGT-internal; not for public release |
|
|
14
|
+
|
|
15
|
+
Rule of thumb: code is public, data is not. Before committing anything, confirm
|
|
16
|
+
it contains no vendor records and no intermediate file from which they could be
|
|
17
|
+
recovered.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artur Sepp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: privateassets
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Quantitative analytics for private-asset returns: multi-factor money-weighted PME (MATF), unsmoothing, and risk-adjusted performance estimation.
|
|
5
|
+
Author: Artur Sepp
|
|
6
|
+
Project-URL: Homepage, https://github.com/ArturSepp/privateassets
|
|
7
|
+
Keywords: private equity,private credit,private assets,PME,direct alpha,risk-adjusted returns,unsmoothing
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE.txt
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: scipy
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Requires-Dist: cvxpy
|
|
16
|
+
Requires-Dist: openpyxl
|
|
17
|
+
Requires-Dist: qis
|
|
18
|
+
Requires-Dist: factorlasso
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# privateassets
|
|
22
|
+
|
|
23
|
+
Quantitative analytics for **private-asset returns** — private equity, private
|
|
24
|
+
credit, and other private-market strategies.
|
|
25
|
+
|
|
26
|
+
Core method: **MATF** — a multi-factor, money-weighted PME that estimates
|
|
27
|
+
risk-adjusted alpha and systematic factor exposures (β) directly from private-asset
|
|
28
|
+
cash flows, generalising Direct Alpha / KS-PME / GPME from a single benchmark to a
|
|
29
|
+
multi-factor deflator. Additional analytics (unsmoothing, volatility/beta
|
|
30
|
+
estimation) currently live within `matf` and may be promoted to top-level
|
|
31
|
+
submodules as they generalise.
|
|
32
|
+
|
|
33
|
+
Built on [`qis`](https://pypi.org/project/qis/) and
|
|
34
|
+
[`factorlasso`](https://pypi.org/project/factorlasso/); it does **not** depend on
|
|
35
|
+
`optimalportfolios` (sibling, not child).
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install privateassets # once published
|
|
41
|
+
# or, from source:
|
|
42
|
+
pip install -e .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import privateassets
|
|
47
|
+
from privateassets.matf import ... # estimator API
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Layout
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
privateassets/ # MATF estimator engine (deflator, PME, panel-MLE
|
|
54
|
+
│ ├── __init__.py # unsmoothing, rolling covar, shrinkage, pipeline)
|
|
55
|
+
│ └── matf/
|
|
56
|
+
│ └── illustrations/ # 14 figure scripts (python -m privateassets.matf <id>)
|
|
57
|
+
scripts/ # reproduction runners
|
|
58
|
+
paper_code/ # publication tracks: jfqa_matf_pme, faj_application (unwritten)
|
|
59
|
+
data/ # git-ignored, private: licensed inputs (see DATA_README.md)
|
|
60
|
+
outputs/ # git-ignored: generated artifacts
|
|
61
|
+
projects/ # git-ignored, private: mandate engagements (not part of the package)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Papers
|
|
65
|
+
|
|
66
|
+
`paper_code/` holds the clean, public publication tracks. The JFQA paper
|
|
67
|
+
(estimator + theory, MSCI cohort data) and the FAJ paper (fund-level application,
|
|
68
|
+
Preqin data) are not yet written. The precursor Oaktree manuscript runs on
|
|
69
|
+
proprietary LP data and is kept in the private `projects/` tree, out of this
|
|
70
|
+
public package.
|
|
71
|
+
|
|
72
|
+
## Data
|
|
73
|
+
|
|
74
|
+
No data is stored in this repository. All vendor / LP data is obtained under
|
|
75
|
+
licence and placed in `data/` (git-ignored). See `DATA_README.md` for the
|
|
76
|
+
per-source licensing rules before adding or committing anything.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# privateassets
|
|
2
|
+
|
|
3
|
+
Quantitative analytics for **private-asset returns** — private equity, private
|
|
4
|
+
credit, and other private-market strategies.
|
|
5
|
+
|
|
6
|
+
Core method: **MATF** — a multi-factor, money-weighted PME that estimates
|
|
7
|
+
risk-adjusted alpha and systematic factor exposures (β) directly from private-asset
|
|
8
|
+
cash flows, generalising Direct Alpha / KS-PME / GPME from a single benchmark to a
|
|
9
|
+
multi-factor deflator. Additional analytics (unsmoothing, volatility/beta
|
|
10
|
+
estimation) currently live within `matf` and may be promoted to top-level
|
|
11
|
+
submodules as they generalise.
|
|
12
|
+
|
|
13
|
+
Built on [`qis`](https://pypi.org/project/qis/) and
|
|
14
|
+
[`factorlasso`](https://pypi.org/project/factorlasso/); it does **not** depend on
|
|
15
|
+
`optimalportfolios` (sibling, not child).
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install privateassets # once published
|
|
21
|
+
# or, from source:
|
|
22
|
+
pip install -e .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import privateassets
|
|
27
|
+
from privateassets.matf import ... # estimator API
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Layout
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
privateassets/ # MATF estimator engine (deflator, PME, panel-MLE
|
|
34
|
+
│ ├── __init__.py # unsmoothing, rolling covar, shrinkage, pipeline)
|
|
35
|
+
│ └── matf/
|
|
36
|
+
│ └── illustrations/ # 14 figure scripts (python -m privateassets.matf <id>)
|
|
37
|
+
scripts/ # reproduction runners
|
|
38
|
+
paper_code/ # publication tracks: jfqa_matf_pme, faj_application (unwritten)
|
|
39
|
+
data/ # git-ignored, private: licensed inputs (see DATA_README.md)
|
|
40
|
+
outputs/ # git-ignored: generated artifacts
|
|
41
|
+
projects/ # git-ignored, private: mandate engagements (not part of the package)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Papers
|
|
45
|
+
|
|
46
|
+
`paper_code/` holds the clean, public publication tracks. The JFQA paper
|
|
47
|
+
(estimator + theory, MSCI cohort data) and the FAJ paper (fund-level application,
|
|
48
|
+
Preqin data) are not yet written. The precursor Oaktree manuscript runs on
|
|
49
|
+
proprietary LP data and is kept in the private `projects/` tree, out of this
|
|
50
|
+
public package.
|
|
51
|
+
|
|
52
|
+
## Data
|
|
53
|
+
|
|
54
|
+
No data is stored in this repository. All vendor / LP data is obtained under
|
|
55
|
+
licence and placed in `data/` (git-ignored). See `DATA_README.md` for the
|
|
56
|
+
per-source licensing rules before adding or committing anything.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""privateassets — quantitative analytics for private-asset returns.
|
|
2
|
+
|
|
3
|
+
Core method: MATF — multi-factor, money-weighted PME (risk-adjusted alpha and
|
|
4
|
+
factor exposures from private-asset cash flows). Additional analytics
|
|
5
|
+
(unsmoothing, volatility/beta estimation) currently live under `matf` and may
|
|
6
|
+
be promoted to top-level submodules as they generalise.
|
|
7
|
+
|
|
8
|
+
Built on `qis` and `factorlasso`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
privateassets.matf — multi-factor, money-weighted PME.
|
|
3
|
+
|
|
4
|
+
Estimates risk-adjusted alpha and systematic factor exposures (beta) directly
|
|
5
|
+
from private-asset cash flows. Generalises Direct Alpha, KS-PME and GPME from a
|
|
6
|
+
single benchmark to a tradable multi-factor deflator, with sign-coherent
|
|
7
|
+
shrinkage betas (factorlasso / HCGL), panel-MLE AR(1) unsmoothing, and
|
|
8
|
+
block-bootstrap inference.
|
|
9
|
+
|
|
10
|
+
Modules
|
|
11
|
+
-------
|
|
12
|
+
_pme.py KS-PME, Direct Alpha, Long-Nickels, and XIRR
|
|
13
|
+
_pipeline.py core estimator: shrinkage betas, deflator, Direct Alpha ladder
|
|
14
|
+
_deflator.py rolling-Sigma multi-factor deflator
|
|
15
|
+
_unsmooth.py fixed-theta AR(1) Getmansky-Lo-Makarov unsmoothing
|
|
16
|
+
_panel_mle.py panel MLE for the AR(1) unsmoothing coefficient
|
|
17
|
+
_rolling_covar.py qis-canonical rolling EWMA factor covariance
|
|
18
|
+
|
|
19
|
+
Data and outputs
|
|
20
|
+
----------------
|
|
21
|
+
Run from the repository root. Inputs are read from data/ and results written to
|
|
22
|
+
outputs/, both resolved against the current working directory (override with
|
|
23
|
+
PRIVATEASSETS_ROOT). Input data is licensed and is never shipped with the
|
|
24
|
+
package. See DATA_README.md.
|
|
25
|
+
|
|
26
|
+
Built on qis and factorlasso. No dependency on optimalportfolios.
|
|
27
|
+
"""
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
# Repo root: where data/ and outputs/ live. Default is the current working
|
|
32
|
+
# directory (run the CLI from the repo root); override with PRIVATEASSETS_ROOT.
|
|
33
|
+
# Never resolved relative to the installed package, so a pip-installed copy
|
|
34
|
+
# does not read from or write into site-packages.
|
|
35
|
+
PROJECT_ROOT = Path(os.environ.get("PRIVATEASSETS_ROOT", os.getcwd())).resolve()
|
|
36
|
+
DATA_DIR = PROJECT_ROOT / 'data'
|
|
37
|
+
OUTPUT_DIR = PROJECT_ROOT / 'outputs'
|
|
38
|
+
|
|
39
|
+
# Ensure outputs dir exists at import time
|
|
40
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
# Convenience handles to the three data files
|
|
43
|
+
DATA_XLSX = DATA_DIR / 'data.xlsx'
|
|
44
|
+
FACTORS_CSV = DATA_DIR / 'factors.csv'
|
|
45
|
+
RF_CSV = DATA_DIR / 'rf_rate.csv'
|
|
46
|
+
|
|
47
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rolling-Sigma multi-factor deflator.
|
|
3
|
+
|
|
4
|
+
Builds the multi-factor PME deflator
|
|
5
|
+
R^b_h = exp{ rf*h + beta'r_h + (1/2)(beta'diag(Sigma_h) - beta'Sigma_h beta) }
|
|
6
|
+
with a time-varying factor covariance Sigma_h. At each quarter-end the
|
|
7
|
+
covariance is an EWMA estimate on a trailing window of monthly factor returns
|
|
8
|
+
(span 36 months), so the deflator at a cash flow date uses only information
|
|
9
|
+
available by that date.
|
|
10
|
+
|
|
11
|
+
Specification:
|
|
12
|
+
- Monthly factor returns: end-of-month level to log excess return
|
|
13
|
+
- EWMA span: 36 months (about a 3-year half-life)
|
|
14
|
+
- Quarterly covariance: monthly Sigma times 3
|
|
15
|
+
- Burn-in: 36 months before the first cash flow date
|
|
16
|
+
|
|
17
|
+
Author: A. Sepp / the desk
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Dict
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
from factorlasso import compute_ewm_covar
|
|
26
|
+
|
|
27
|
+
from privateassets.matf._pipeline import (
|
|
28
|
+
cf_with_terminal_for_vintage,
|
|
29
|
+
vintage_da_fast,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def factor_monthly_log_returns(levels_daily: pd.DataFrame) -> pd.DataFrame:
|
|
34
|
+
"""End-of-month log returns from daily levels."""
|
|
35
|
+
me = levels_daily.resample('ME').last()
|
|
36
|
+
return np.log(me / me.shift(1)).dropna(how='all')
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def rolling_ewma_quarterly_covar(
|
|
40
|
+
levels_daily: pd.DataFrame,
|
|
41
|
+
quarter_ends: pd.DatetimeIndex,
|
|
42
|
+
span_months: int = 36,
|
|
43
|
+
burnin_months: int = 36,
|
|
44
|
+
) -> Dict[pd.Timestamp, np.ndarray]:
|
|
45
|
+
"""
|
|
46
|
+
Build a dict mapping each quarter-end to an EWMA covariance matrix
|
|
47
|
+
estimated on monthly factor returns with span = span_months.
|
|
48
|
+
|
|
49
|
+
The returned Σ are SCALED TO A QUARTERLY HORIZON: monthly Σ × 3
|
|
50
|
+
(the standard variance-time-scaling under iid). This is consistent
|
|
51
|
+
with how the deflator uses Σ_h (h = 1 quarter for adjacent cash flow
|
|
52
|
+
dates within the same quarter).
|
|
53
|
+
"""
|
|
54
|
+
monthly_log = factor_monthly_log_returns(levels_daily)
|
|
55
|
+
|
|
56
|
+
sigmas = {}
|
|
57
|
+
for q in quarter_ends:
|
|
58
|
+
# Monthly returns up to and including this quarter end
|
|
59
|
+
m_end = q.to_period('M').to_timestamp(how='end').normalize()
|
|
60
|
+
window = monthly_log.loc[:m_end].dropna(how='any')
|
|
61
|
+
if len(window) < burnin_months:
|
|
62
|
+
continue
|
|
63
|
+
# EWMA covariance on monthly excess log returns, demean within the window
|
|
64
|
+
Xv = window.values
|
|
65
|
+
Xv_dm = Xv - Xv.mean(axis=0)
|
|
66
|
+
sigma_monthly = compute_ewm_covar(a=Xv_dm, span=span_months)
|
|
67
|
+
sigma_quarterly = sigma_monthly * 3.0 # variance-time scaling
|
|
68
|
+
sigmas[q] = sigma_quarterly
|
|
69
|
+
return sigmas
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def matf_deflator_rolling(
|
|
73
|
+
cf_dates: list,
|
|
74
|
+
t0: pd.Timestamp,
|
|
75
|
+
cum_log_factor_q: np.ndarray, # (T, M) cumulative log factor returns at QE
|
|
76
|
+
cum_log_rf_q: np.ndarray, # (T,) cumulative log(1+rf) at QE
|
|
77
|
+
quarters_q: pd.DatetimeIndex,
|
|
78
|
+
quarters_ns: np.ndarray,
|
|
79
|
+
beta_vec: np.ndarray,
|
|
80
|
+
sigma_h_dict: Dict[pd.Timestamp, np.ndarray],
|
|
81
|
+
sigma_h_default: np.ndarray,
|
|
82
|
+
) -> np.ndarray:
|
|
83
|
+
"""
|
|
84
|
+
Deflator with quarter-specific Σ_h pulled from sigma_h_dict.
|
|
85
|
+
For each cash flow date d, find the most recent quarter-end ≤ d,
|
|
86
|
+
look up Σ_h for that quarter (or use sigma_h_default if missing
|
|
87
|
+
due to burn-in), then evaluate Eq 3.
|
|
88
|
+
"""
|
|
89
|
+
cf_ns = np.array([pd.Timestamp(t).value for t in cf_dates], dtype=np.int64)
|
|
90
|
+
t0_ns = pd.Timestamp(t0).value
|
|
91
|
+
n_q = len(quarters_ns)
|
|
92
|
+
|
|
93
|
+
idx_t = np.clip(np.searchsorted(quarters_ns, cf_ns, side='right') - 1,
|
|
94
|
+
0, n_q - 1)
|
|
95
|
+
idx_t0 = int(np.clip(np.searchsorted(quarters_ns, t0_ns, side='right') - 1,
|
|
96
|
+
0, n_q - 1))
|
|
97
|
+
|
|
98
|
+
rh = cum_log_factor_q[idx_t] - cum_log_factor_q[idx_t0] # (n_cf, M)
|
|
99
|
+
rfh = cum_log_rf_q[idx_t] - cum_log_rf_q[idx_t0] # (n_cf,)
|
|
100
|
+
|
|
101
|
+
h_years = np.array([max((pd.Timestamp(t) - pd.Timestamp(t0)).days, 1) / 365.25
|
|
102
|
+
for t in cf_dates])
|
|
103
|
+
h_q = h_years * 4.0
|
|
104
|
+
|
|
105
|
+
out = np.empty(len(cf_dates), dtype=float)
|
|
106
|
+
for j, t in enumerate(cf_dates):
|
|
107
|
+
q_end = quarters_q[idx_t[j]]
|
|
108
|
+
sigma = sigma_h_dict.get(q_end, sigma_h_default)
|
|
109
|
+
diag_h = np.diag(sigma)
|
|
110
|
+
quad = float(beta_vec @ sigma @ beta_vec)
|
|
111
|
+
rb_j = (rfh[j]
|
|
112
|
+
+ rh[j] @ beta_vec
|
|
113
|
+
+ 0.5 * h_q[j] * (beta_vec @ diag_h)
|
|
114
|
+
- 0.5 * h_q[j] * quad)
|
|
115
|
+
out[j] = np.exp(rb_j)
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def per_vintage_da_with_rolling_covar(
|
|
120
|
+
cf: pd.DataFrame,
|
|
121
|
+
navs: pd.DataFrame,
|
|
122
|
+
asof: pd.Timestamp,
|
|
123
|
+
cum_log_factor_q: np.ndarray,
|
|
124
|
+
cum_log_rf_q: np.ndarray,
|
|
125
|
+
quarters_q: pd.DatetimeIndex,
|
|
126
|
+
quarters_ns: np.ndarray,
|
|
127
|
+
beta_arr: np.ndarray,
|
|
128
|
+
sigma_h_dict: Dict[pd.Timestamp, np.ndarray],
|
|
129
|
+
sigma_h_default: np.ndarray,
|
|
130
|
+
) -> pd.DataFrame:
|
|
131
|
+
rows = []
|
|
132
|
+
for v in sorted(cf['vintage_label'].unique()):
|
|
133
|
+
cf_g = cf[cf['vintage_label'] == v]
|
|
134
|
+
nav_g = navs[navs['vintage_label'] == v]
|
|
135
|
+
cf_v, rvpi_nav, _, dates = cf_with_terminal_for_vintage(cf_g, nav_g, asof)
|
|
136
|
+
defl = matf_deflator_rolling(
|
|
137
|
+
dates, dates[0], cum_log_factor_q, cum_log_rf_q,
|
|
138
|
+
quarters_q, quarters_ns, beta_arr, sigma_h_dict, sigma_h_default)
|
|
139
|
+
da = vintage_da_fast(cf_v, rvpi_nav, dates, defl)
|
|
140
|
+
contrib = -cf_v.loc[cf_v['amount'] < 0, 'amount'].sum()
|
|
141
|
+
distrib = cf_v.loc[cf_v['amount'] > 0, 'amount'].sum()
|
|
142
|
+
dpi = distrib / contrib if contrib > 0 else float('nan')
|
|
143
|
+
rows.append({'vintage_label': v, 'contribution': contrib, 'DPI': dpi,
|
|
144
|
+
'DA': da})
|
|
145
|
+
return pd.DataFrame(rows)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def per_vintage_da_with_constant_covar(
|
|
149
|
+
cf: pd.DataFrame,
|
|
150
|
+
navs: pd.DataFrame,
|
|
151
|
+
asof: pd.Timestamp,
|
|
152
|
+
cum_log_factor_q: np.ndarray,
|
|
153
|
+
cum_log_rf_q: np.ndarray,
|
|
154
|
+
quarters_q: pd.DatetimeIndex,
|
|
155
|
+
quarters_ns: np.ndarray,
|
|
156
|
+
beta_arr: np.ndarray,
|
|
157
|
+
sigma_h_const: np.ndarray,
|
|
158
|
+
) -> pd.DataFrame:
|
|
159
|
+
rows = []
|
|
160
|
+
diag_h_q = np.diag(sigma_h_const)
|
|
161
|
+
quad = float(beta_arr @ sigma_h_const @ beta_arr)
|
|
162
|
+
for v in sorted(cf['vintage_label'].unique()):
|
|
163
|
+
cf_g = cf[cf['vintage_label'] == v]
|
|
164
|
+
nav_g = navs[navs['vintage_label'] == v]
|
|
165
|
+
cf_v, rvpi_nav, _, dates = cf_with_terminal_for_vintage(cf_g, nav_g, asof)
|
|
166
|
+
cf_ns = np.array([pd.Timestamp(t).value for t in dates], dtype=np.int64)
|
|
167
|
+
n_q = len(quarters_ns)
|
|
168
|
+
idx_t = np.clip(np.searchsorted(quarters_ns, cf_ns, side='right') - 1,
|
|
169
|
+
0, n_q - 1)
|
|
170
|
+
idx_t0 = idx_t[0]
|
|
171
|
+
rh = cum_log_factor_q[idx_t] - cum_log_factor_q[idx_t0]
|
|
172
|
+
rfh = cum_log_rf_q[idx_t] - cum_log_rf_q[idx_t0]
|
|
173
|
+
h_years = np.array([
|
|
174
|
+
max((pd.Timestamp(t) - pd.Timestamp(dates[0])).days, 1) / 365.25
|
|
175
|
+
for t in dates])
|
|
176
|
+
h_q = h_years * 4.0
|
|
177
|
+
rb = (rfh + rh @ beta_arr
|
|
178
|
+
+ 0.5 * h_q * (beta_arr @ diag_h_q)
|
|
179
|
+
- 0.5 * h_q * quad)
|
|
180
|
+
defl = np.exp(rb)
|
|
181
|
+
da = vintage_da_fast(cf_v, rvpi_nav, dates, defl)
|
|
182
|
+
contrib = -cf_v.loc[cf_v['amount'] < 0, 'amount'].sum()
|
|
183
|
+
distrib = cf_v.loc[cf_v['amount'] > 0, 'amount'].sum()
|
|
184
|
+
dpi = distrib / contrib if contrib > 0 else float('nan')
|
|
185
|
+
rows.append({'vintage_label': v, 'contribution': contrib, 'DPI': dpi,
|
|
186
|
+
'DA': da})
|
|
187
|
+
return pd.DataFrame(rows)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Panel MLE for common AR(1) coefficient across vintages.
|
|
3
|
+
|
|
4
|
+
Model: each vintage i has quarterly NAV-implied returns r_{i,t} that
|
|
5
|
+
follow:
|
|
6
|
+
r_{i,t} = θ · r_{i,t-1} + ε_{i,t}, ε_{i,t} ~ N(0, σ_i²)
|
|
7
|
+
|
|
8
|
+
with COMMON AR coefficient θ shared across vintages, but idiosyncratic
|
|
9
|
+
residual variances σ_i².
|
|
10
|
+
|
|
11
|
+
Profile-out variances and reduce to a 1-D optimization in θ. With
|
|
12
|
+
σ_i² profiled at MLE σ̂_i²(θ) = mean_t (r_{i,t} - θ r_{i,t-1})², the
|
|
13
|
+
profile log-likelihood for θ is:
|
|
14
|
+
|
|
15
|
+
L_p(θ) = -(1/2) Σ_i n_i log σ̂_i²(θ) + const
|
|
16
|
+
|
|
17
|
+
Maximize over θ ∈ (-1, 1).
|
|
18
|
+
|
|
19
|
+
Compare to:
|
|
20
|
+
- Production pooled AR(1): θ_pooled = 0.142
|
|
21
|
+
- Per-vintage average: avg θ_i = 0.136
|
|
22
|
+
|
|
23
|
+
Author: A. Sepp / the desk
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
from typing import Dict
|
|
27
|
+
import numpy as np
|
|
28
|
+
from scipy.optimize import minimize_scalar
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def panel_ar1_neg_log_likelihood(
|
|
32
|
+
theta: float,
|
|
33
|
+
series_list: list, # list of (label, np.ndarray) per vintage, demeaned
|
|
34
|
+
) -> float:
|
|
35
|
+
"""
|
|
36
|
+
Profile-out negative log-likelihood for panel AR(1) with common θ
|
|
37
|
+
and per-vintage idiosyncratic σ_i². Series are pre-demeaned.
|
|
38
|
+
|
|
39
|
+
Returns: -L_p(θ) where L_p is the profile log-likelihood.
|
|
40
|
+
"""
|
|
41
|
+
nll = 0.0
|
|
42
|
+
for item in series_list:
|
|
43
|
+
r = item[1] if isinstance(item, tuple) else item
|
|
44
|
+
if len(r) < 3:
|
|
45
|
+
continue
|
|
46
|
+
eps = r[1:] - theta * r[:-1]
|
|
47
|
+
n_i = len(eps)
|
|
48
|
+
sigma2_hat = np.mean(eps ** 2)
|
|
49
|
+
if sigma2_hat <= 0:
|
|
50
|
+
return np.inf
|
|
51
|
+
nll += 0.5 * n_i * np.log(sigma2_hat)
|
|
52
|
+
return nll
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def panel_ar1_grad(theta: float, series_list: list) -> float:
|
|
56
|
+
"""Numerical derivative of NLL wrt theta (used for diagnostics)."""
|
|
57
|
+
eps = 1e-6
|
|
58
|
+
return (panel_ar1_neg_log_likelihood(theta + eps, series_list)
|
|
59
|
+
- panel_ar1_neg_log_likelihood(theta - eps, series_list)) / (2 * eps)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fisher_info_panel_ar1(theta_hat: float, series_list: list) -> float:
|
|
63
|
+
"""
|
|
64
|
+
Approximate Fisher information for theta from the panel.
|
|
65
|
+
For AR(1) with σ_i² known, the per-obs Fisher info is
|
|
66
|
+
Var(r_{i,t-1}) / σ_i². With σ_i² profiled at MLE this becomes
|
|
67
|
+
n_i · Var(r_{i,t-1}) / sum((r_{i,t} - θ r_{i,t-1})²).
|
|
68
|
+
"""
|
|
69
|
+
fisher = 0.0
|
|
70
|
+
for item in series_list:
|
|
71
|
+
# series_list is list of (label, array)
|
|
72
|
+
r = item[1] if isinstance(item, tuple) else item
|
|
73
|
+
if len(r) < 3:
|
|
74
|
+
continue
|
|
75
|
+
eps = r[1:] - theta_hat * r[:-1]
|
|
76
|
+
sigma2_hat = np.mean(eps ** 2)
|
|
77
|
+
if sigma2_hat <= 0:
|
|
78
|
+
continue
|
|
79
|
+
var_lag = np.var(r[:-1])
|
|
80
|
+
n_i = len(eps)
|
|
81
|
+
fisher += n_i * var_lag / sigma2_hat
|
|
82
|
+
return fisher
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def fit_panel_ar1(series_list: list) -> Dict:
|
|
86
|
+
"""
|
|
87
|
+
Fit common-θ panel AR(1) by profile MLE, return θ_hat and
|
|
88
|
+
asymptotic standard error.
|
|
89
|
+
"""
|
|
90
|
+
res = minimize_scalar(
|
|
91
|
+
panel_ar1_neg_log_likelihood,
|
|
92
|
+
args=(series_list,),
|
|
93
|
+
bounds=(-0.95, 0.95),
|
|
94
|
+
method='bounded',
|
|
95
|
+
options={'xatol': 1e-6},
|
|
96
|
+
)
|
|
97
|
+
theta_hat = float(res.x)
|
|
98
|
+
fisher = fisher_info_panel_ar1(theta_hat, series_list)
|
|
99
|
+
se = float(1.0 / np.sqrt(fisher)) if fisher > 0 else float('nan')
|
|
100
|
+
|
|
101
|
+
# Also compute per-vintage MLE for comparison
|
|
102
|
+
per_vintage = {}
|
|
103
|
+
for v, r in series_list:
|
|
104
|
+
if len(r) < 5:
|
|
105
|
+
per_vintage[v] = None
|
|
106
|
+
continue
|
|
107
|
+
# Per-vintage AR(1) MLE = OLS of r_t on r_{t-1}
|
|
108
|
+
y = r[1:]; x = r[:-1]
|
|
109
|
+
if np.var(x) < 1e-10:
|
|
110
|
+
per_vintage[v] = None
|
|
111
|
+
continue
|
|
112
|
+
theta_i = float(np.cov(y, x)[0, 1] / np.var(x))
|
|
113
|
+
per_vintage[v] = theta_i
|
|
114
|
+
|
|
115
|
+
# n_total
|
|
116
|
+
n_total = sum(max(0, len(r) - 1) for _, r in series_list)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
'theta_hat': theta_hat,
|
|
120
|
+
'se': se,
|
|
121
|
+
'neg_log_lik': float(res.fun),
|
|
122
|
+
'fisher_info': fisher,
|
|
123
|
+
'per_vintage_theta': per_vintage,
|
|
124
|
+
'n_total': n_total,
|
|
125
|
+
'n_vintages': len([1 for _, r in series_list if len(r) >= 5]),
|
|
126
|
+
}
|