PARIS — Portfolio Analytics, Risk & Investment Statistics
Performance statistics for a fund or a collection of funds, on an absolute basis and relative to a benchmark. Pure Python (numpy + pandas), one small module per topic, and every number cross-checked against independent reference implementations and public portfolio tools.
Installation
Installs and runs on Linux, macOS and Windows with Python >= 3.11.
pip install paris-analytics # or: uv add paris-analytics (the import name is `paris`)
pip install "paris-analytics[scipy]" # optional extra: p-values in regression_stats
pip install "paris-analytics[polars]" # optional extra: accept polars frames as input
Quick start
The package ships sample data (see Sample data below), so every example
runs out of the box — import paris also exposes paris.data:
import paris
m = paris.data.load_managers() # monthly total returns, 2010-2025: six funds, three benchmarks, T-bill
funds, spx, rf = m.iloc[:, :6], m["SPY"], m["TBILL3M"]
paris.sharpe(funds, rf=rf) # one value per fund (Series); a Series input returns a float
paris.max_drawdown(funds)
paris.drawdown_table(m["FCNTX"], top=5)
paris.beta(funds, spx) # benchmark-relative: pass the benchmark second
paris.information_ratio(funds, spx)
paris.up_capture(funds, spx)
paris.period_returns(funds) # MTD, QTD, YTD, 1Y, 3Y, 5Y, 10Y, ITD
paris.stats(funds, benchmark=spx, rf=rf) # the whole table, metrics x funds (benchmark last)
paris.calendar_table(m["FCNTX"], spx) # years x Jan..Dec, Annual, SPY (tables: rows = metrics/periods, cols = funds)
paris.downside_table(funds, spx, rf=rf) # also capture_, distribution_, annualized_, drawdown_ratio_table, drawdown_summary
paris.rolling(funds, paris.sharpe, 36, rf=rf) # any scalar function over a 36-observation window, date x fund
pf = paris.Portfolio(funds, benchmark=spx, rf=rf)
pf.sortino(), pf.alpha(), pf.stats(), pf.calendar_table(), pf.rolling(paris.beta, 36)
w = [0.3, 0.2, 0.2, 0.1, 0.1, 0.1] # weights: one-time vector (or a DataFrame of dated rows)
paris.portfolio_return(funds, w, rebalance="QE") # buy-and-hold drift between quarterly rebalances
paris.contribution(funds, w) # BOP weight x return per asset; period_contributions() links spans
sleeves = funds[["FCNTX", "DODGX"]].set_axis(["Growth", "Value"], axis=1) # Brinson categories match by column name
style = m[["IWF", "IWD"]].set_axis(["Growth", "Value"], axis=1)
paris.brinson(sleeves, [0.6, 0.4], style, [0.5, 0.5]) # allocation / selection / interaction per category
paris.volatility_contribution(funds, w, pct=True) # Euler risk shares; also var_/cvar_contribution, marginal_var
paris.Portfolio(funds, weights=w, benchmark=spx, rf=rf).sharpe() # every stat on the weighted portfolio
Inputs: pandas Series/DataFrame with a DatetimeIndex (daily, weekly, monthly, quarterly, yearly —
frequency is inferred, override with periods_per_year), numpy arrays, or polars frames (with the
polars extra). Series are trimmed to their common window; interior gaps raise GapError
(nothing is ever filled). A scalar rf is an annual rate; a Series rf (like TBILL3M
above) is per-period. Drawdowns are negative; VaR/CVaR are returned as (negative) returns.
Every function's docstring states its formula and the convention behind each keyword switch
(help(paris.sharpe)). A guided tour of the most used statistics, one cell each, is in
notebooks/paris_tour.ipynb
(committed with its outputs, so it reads without running; to run it, open it in any Jupyter whose
kernel has paris installed).
Sample data (paris.data)
PARIS ships two small frozen datasets that every example uses:
| Loader | Contents | Window |
|---|---|---|
paris.data.load_managers() |
monthly simple total returns of six widely held active US large-cap funds (FCNTX Fidelity Contrafund, AGTHX Growth Fund of America, FMAGX Fidelity Magellan, AMCPX AMCAP, DODGX Dodge & Cox Stock, PRGFX T. Rowe Price Growth Stock), three total-return benchmark proxies (SPY, IWF, IWD — ETFs, so net of their fees) and TBILL3M, the 3-month Treasury bill yield per month for use as a per-period rf |
192 month-ends, 2010-01-31 – 2025-12-31 |
paris.data.load_prices() |
daily total-return index levels of SPY and FCNTX (.pct_change() for returns) |
1,255 trading days, 2021-01-04 – 2025-12-31 |
paris.data.describe() lists every column with its full name and role. Total returns reinvest each
distribution on its ex-date; the frames are rectangular (no missing values anywhere), so they pass
GapError checks whole. The data is illustrative — it plays no part in the validation evidence. The
frozen CSVs ship inside the package (src/paris/data/managers.csv, prices.csv); nothing
vendor-specific ships.
Modules (copy one file at a time — each depends only on _core.py)
| Module | Contents |
|---|---|
returns.py |
total / annualised return, CAGR, cumulative & wealth index, period returns, calendar tables, best/worst, win rate, streaks, excess & active returns |
risk.py |
volatility, downside/upside/semi deviation, skewness, kurtosis, VaR & CVaR (historical, gaussian, Cornish-Fisher), tail & outlier ratios |
drawdown.py |
drawdown series & episode table, max/average/longest drawdown, ulcer & pain index, Calmar, Sterling, Burke, Pain, Martin ratios, recovery factor |
ratios.py |
Sharpe (incl. VaR/ES-based, smart, probabilistic, adjusted), Sortino, Omega, Kappa, upside potential, profit factor, gain-to-pain, payoff, CPC, common-sense, Kelly, risk of ruin, prospect, serenity |
relative.py |
beta/alpha (CAPM, bull/bear, timing), Jensen, Treynor, tracking error, information ratio, active premium, systematic/specific/total risk, appraisal, Fama beta, selectivity, M², capture & number/percentage ratios, batting average, regression table |
tables.py |
capture, downside, distribution, annualised-returns, calendar (month grid + annual), drawdown summary and ratio tables, rolling(fn, window) — every cell is a call to a topic-module function (imports the topic modules) |
attribution.py |
weights → portfolio return with drift / rebalancing, per-period contributions, BOP/EOP weights, linked multi-period contributions, active contribution, Brinson attribution (BF/BHB; Carino, Menchero or arithmetic linking) |
budgeting.py |
Euler contributions to volatility, VaR (Gaussian, Cornish-Fisher) and CVaR (historical, Gaussian, Cornish-Fisher) for one weight vector; marginal VaR / CVaR |
summary.py / portfolio.py |
stats() table and the Portfolio convenience wrapper (these two import all topic modules) |
Defaults follow the industry-standard R reference package; every convention that differs between
the reference implementations and the public portfolio tools is a named keyword switch (ddof,
method, annualize, geometric, compounding, ...) rather than a silent choice.
Validation
The development suite behind each release holds 1,016 automated tests (24 documented skips): 2,080 values reproduced from the R reference package at rtol 1e-7 plus 20 drawdown tables and the full set of summary tables; 49 functions of the Python reference package at rtol 1e-6; two public web tools at their display precision plus an independent-data check; hypothesis-based invariants (drawdown bounds, return identities, scale invariance, tail-risk ordering, regression recovery); 99–100 % line and branch coverage on the metric modules. Where a reference disagrees with PARIS its source was read and the case excluded by name with a written reason — tolerances are never widened to make a test pass.
Testing
uv sync # or: pip install pytest (add --all-extras for scipy p-values)
uv run pytest # ~15 s, no network
Every public function is run on the shipped sample data and compared with its frozen result in
tests/expected/ (one JSON file per
module, one value per line); the suite also checks input/output shapes (Series in, one number out;
DataFrame in, one value per column), the error paths (GapError on interior gaps, weight
validation, bad switch values) and the NaN/inf conventions on degenerate input. The frozen results
are a regression net — they are generated from this package by python -m tests.generate_expected
and pin every number across releases and platforms.
Release history: CHANGELOG.md.
Disclaimer
PARIS is a Python library for internal analytical, educational and research
use only. It does not provide investment, tax, legal or other professional
advice, and its output must not be relied upon for investment, trading,
reporting or any other decision-making. The library is provided "AS IS"
without warranty of any kind under the MIT License. Users are solely
responsible for independently verifying every number, method and convention
before use. PARIS is not a validated model and is not certified for regulated
use. See DISCLAIMER.md
for the full statement.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file paris_analytics-0.5.1-py3-none-any.whl.
File metadata
- Download URL: paris_analytics-0.5.1-py3-none-any.whl
- Upload date:
- Size: 80.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
efa9d7d1cc2bd20d45635ce05184955d30afabb1edaab86aa13def2a4a55dca6
|
|
| MD5 |
722ec69b04175f2f48ecce79e247467f
|
|
| BLAKE2b-256 |
5e84da86c202c553569a233d46feace2f39b911dc94e00a7568dc4ba448c4f3e
|