A public-market-equivalent (PME) analysis engine for portfolios of private drawdown funds (capital calls and distributions). It ingests per-fund cash flows, computes PME and return metrics against daily benchmark series, FX-converts non-USD funds, ranks results against vintage quartiles, builds a fund-of-funds composite, and renders a branded PDF and Excel report plus a methodology guide.
- All data here is fictional. Every fund, manager, client, commitment, cash flow, benchmark price, and FX rate is synthetic and for demonstration only. The firm ("Hypothetical Capital Partners") and client ("Sample Client") are invented. Public ETF ticker symbols are kept (they are public), but all price and return values are a seeded random walk, not licensed market data.
- The proprietary calculation engine is intentionally withheld. The modules that implement the analytics are present as documented stubs that raise
NotImplementedError. This repository shows the full architecture, data model, ingestion, configuration, templates, sample-data generation, and example output, but not the formulas. The engine is available under a commercial or consulting license.
The analytics implement established techniques. This repository names them and shows their output; it does not reproduce their formulas.
- Public-market equivalent: PME 1 (Kaplan-Schoar), PME 1b (Modified PME), PME 2 (Long-Nickels).
- XIRR via Newton-Raphson with a bisection fallback.
- Excess IRR / direct alpha, MIRR, NAV-to-NAV IRR (rolling horizons), and a capital-deployment factor.
- DPI / TVPI / RVPI multiples and remaining-commitment tracking (with recallable distributions and outside-commitment expenses).
- Vintage quartile ranking by strategy and region.
- FX conversion of benchmark returns for non-USD funds.
- A pooled-cash-flow fund-of-funds composite.
See examples/Methodology_Guide.pdf for a capability-level description of each metric.
ingestion (CSV / Excel / YAML, validated with Pydantic)
-> SQLite data model
-> analytics engine [proprietary, withheld]
-> Jinja2 / WeasyPrint PDF and openpyxl Excel output
| Module | Role | In this repo |
|---|---|---|
src/loader.py, src/models.py |
Ingestion + validation | Runs publicly |
src/excel_config.py |
Excel <-> YAML config round-trip | Runs publicly |
src/benchmarks.py, src/fx.py, src/quartiles.py |
Benchmark / FX / quartile lookups | Runs publicly |
src/report.py, templates/ |
PDF + Excel rendering | Runs publicly |
src/db/ |
SQLite schema + repository | Runs publicly |
src/cli.py |
Command-line interface | Runs publicly |
scripts/generate_sample_data.py |
Synthetic dataset generator | Runs publicly |
src/engine.py |
PME / IRR / KPI / composite orchestration | Stub (licensed) |
src/xirr.py |
XIRR solver | Stub (licensed) |
src/composite.py |
Fund-of-funds composite | Stub (licensed) |
src/core/cash_flow_processor.py |
Sign-convention + period processing | Stub (licensed) |
Every computed figure (multiples, IRRs, PME, composite, quartile ranks) requires the licensed engine. Without it, the report cannot be generated; the committed examples/ show representative output produced before redaction.
- Python 3.11+
pip install -e .(dev extras:pip install -e ".[dev]")- PDF output uses WeasyPrint, which needs its system libraries (the GTK stack on Windows).
CLI entry point: drawdown-pme (or python -m src.cli).
pip install -e .
python scripts/generate_sample_data.py # writes the full synthetic dataset
python -m src.cli validate # ingestion + validation (no engine)
python -m src.cli --help # all commandsIngestion, validation, config round-trip, benchmark/FX/quartile parsing, report
templating, and sample-data generation all run with the public code. The
analysis run (python -m src.cli run) stops at the first call into the withheld
engine and raises NotImplementedError; that path requires a license.
generate_sample_data.py builds ten invented funds chosen to exercise every
report branch, across VC / PE / Real Estate / Real Assets / FoF strategies and
Europe / North America regions, with strategy-appropriate benchmarks (for
example real estate vs a REIT index, energy vs an oil and gas index, growth VC
vs a Nasdaq index, never one index for everything), plus a money-market series
for the risk-free leg. The roster includes active, realized (fully wound down),
evaluation, an archived fund, an early-vintage J-curve fund, a single-period
fund, two EUR funds (FX path), and examples of recallable distributions and
outside-commitment expenses. The generator is seeded, so its output is
byte-reproducible.
config/ fund + quartile + benchmark-currency config (tracked, synthetic)
data/cash_flows/ per-fund cash-flow CSVs (tracked, synthetic)
data/benchmark_returns/ consolidated daily benchmark return CSV (tracked, synthetic)
data/fx_rates/ daily FX series (tracked, synthetic)
data/commentary/ per-fund narrative + fund-terms workbook (tracked, synthetic)
db/sample.sqlite synthetic sample database (tracked, synthetic)
examples/ pre-built sample PDFs + Excel (tracked)
src/ pipeline (engine modules are stubs)
templates/ Jinja2 HTML + placeholder logo
scripts/ synthetic-data generator
tests/ pytest suite (engine tests removed)
output/ generated reports (gitignored)
db/pme.sqlite live benchmark cache (gitignored)
The synthetic config/, data/, and db/sample.sqlite inputs are committed so
the public capabilities run out of the box, and can be regenerated at any time
with python scripts/generate_sample_data.py.
python -m pytest -qThe suite covers the parts that run without the engine: ingestion and validation, config round-trip, benchmark / FX parsing, quartile lookups, and commentary parsing. Tests that exercised the withheld engine (and encoded its expected values) have been removed.
MIT for the public code in this repository. See LICENSE. The proprietary analytics engine is licensed separately.