Skip to main content

Portfolio analytics, MPT optimization, cost-aware backtesting, and walk-forward validation for systematic traders and quant researchers. Not financial advice.

Project description

Quant Reporter

CI PyPI version Downloads Python versions License: MIT

Portfolio analytics, MPT optimization, cost-aware backtesting, and walk-forward validation for systematic traders and quant researchers.

Not financial advice — see Disclaimer. Pulls market data from Yahoo Finance via yfinance (network access — see Data sources & offline use).

quant_reporter turns a plain {ticker: weight} portfolio into rich, interactive, multi-page HTML reports. It is built on pandas, numpy, scipy, statsmodels, yfinance, and plotly, and covers performance & risk analytics, modern portfolio optimization, Monte Carlo forecasting, walk-forward validation, and Fama-French / Brinson attribution.

30-second start

pip install quant-reporter
import quant_reporter as qr

# Build a full multi-page HTML report for a portfolio vs. a benchmark.
qr.create_portfolio_report(
    {"AAPL": 0.6, "MSFT": 0.4}, "SPY",
    "2020-01-01", "2023-12-31",
    filename="report.html",
)
# → open report.html

That's the whole loop: a {ticker: weight} dict in, an interactive report out. Everything below (optimization, backtesting, Black-Litterman, custom data sources) is optional depth.

See it without installing: browse the example report gallery (60/40, All-Weather, Magnificent 7, Bogleheads 3-fund) or run the zero-setup Colab notebook.

Version note: the last release on PyPI was 1.1.1. 2.0.0 was an internal milestone and was never published, so 2.1.0 is the first 2.x release you can pip install. The 1.x → 2.x change is breaking (see Migrating from 1.x).

2.0 introduces a unified ReportContext architecture: every report takes the same inputs — a portfolio, a benchmark, and a training window — fetches data once, and renders. This is a breaking change from 1.x (see Migrating from 1.x).

2.1 adds a primitives-first strategy → backtest → report loop — a cost-aware, walk-forward backtest engine with honest out-of-sample statistics (PSR/DSR) and an interactive backtest report — plus an opt-in recommendation layer (target weights, rebalance trade lists, risk-limit alerts, strategy verdicts, each carrying its rationale & evidence). All additive; the 2.0 API is unchanged. See Strategy backtesting & recommendations.

2.2 adds a decision-support planning layer: a CFA-grounded investor Profile (risk-tolerance presets + return objective + TTLU constraints) that constrains the optimizer and sets the risk-alert thresholds via recommend(profile=), plus walk-forward validation of recommendations (recommend(validate=True) → out-of-sample vs in-sample Sharpe, degradation, and a holds-up / fragile / inconclusive verdict). All additive; the 2.1 API is unchanged.


Who it's for

Question a trader/PM asks What the package answers
Is my portfolio good, risk-adjusted? Portfolio report: Sharpe, Sortino, Calmar, max drawdown, VaR/CVaR, alpha/beta vs a benchmark
How should I weight these? Optimization report: efficient frontier, min-vol, max-Sharpe, sector caps/mins, Risk Parity, HRP, Min-Correlation, Max-Diversification
Am I just overfitting the past? Validation report: train/test out-of-sample split, overfitting score, walk-forward windows
What could happen next? Monte Carlo report: GBM path simulation, success probabilities, time-to-target, day-1 stress shocks
Was it skill or just market beta? Factor report: Fama-French regression (alpha vs factor exposure) + Brinson allocation/selection attribution
How do I fold in my own views? Black-Litterman: blend market equilibrium with absolute & relative views
Which strategy actually holds up out-of-sample? Strategy backtesting (2.1): cost-aware walk-forward backtest/backtest_many, honest PSR/DSR out-of-sample stats, interactive backtest report
What should I do about it? (opt-in) Recommendation layer (2.1): recommended target weights, a rebalance trade list, risk-limit alerts, and a strategy verdict — each with its rationale & evidence
Does this suit my goals, and does it hold up out-of-sample? (opt-in) Decision-support layer (2.2): a CFA-grounded investor Profile that constrains the optimizer and sets alert thresholds, plus walk-forward validation of the recommendation (in-sample vs OOS Sharpe + a holds-up / fragile verdict)

The 2.0 report generators are descriptive analytics on daily historical data — a decision-support and communication tool. 2.1 adds a cost-aware, walk-forward backtest engine, a composable strategy layer, and an opt-in recommendation layer (see Strategy backtesting & recommendations). Monte Carlo assumes Geometric Brownian Motion (thin tails — it understates crash risk), and reports depend on live yfinance data.


Installation

pip install quant-reporter          # from PyPI

For local development (editable install + test tooling):

git clone https://github.com/manan-tech/quant_reporter.git
cd quant_reporter
pip install -e ".[test]"

Requires Python ≥ 3.9.


Quick start

Every report shares the same call shape:

create_<kind>_report(portfolio_dict, benchmark_ticker, train_start, train_end, filename=..., **options)
  • portfolio_dict{ticker: weight} (weights need not sum to 1; they are used as given).
  • benchmark_ticker — e.g. "SPY".
  • train_start / train_end — the in-sample window used to fit optimizers and metrics. The out-of-sample test window is derived automatically (train_end + 1 day … yesterday) and used by the validation report.
import quant_reporter as qr

portfolio = {"AAPL": 0.3, "MSFT": 0.3, "XOM": 0.2, "GLD": 0.2}

# The flagship: one HTML covering all five analyses
qr.create_combined_report(
    portfolio_dict=portfolio,
    benchmark_ticker="SPY",
    train_start="2018-01-01",
    train_end="2023-12-31",
    filename="Combined_Report.html",
    sector_map={"AAPL": "Tech", "MSFT": "Tech", "XOM": "Energy", "GLD": "Commodities"},
    sector_caps={"Tech": 0.5, "Energy": 0.3, "Commodities": 0.3},
    risk_free_rate="auto",   # fetches the live 13-week T-bill rate; or pass a float like 0.045
)

Data sources & offline use

By default quant_reporter fetches prices and the risk-free rate from Yahoo Finance via yfinance. This means a normal report run makes live network calls — relevant for corporate, air-gapped, or compliance-sensitive environments.

All data access goes through a single DataProvider protocol, so you can swap Yahoo for Bloomberg, Refinitiv, a local CSV, or a test fixture without touching any report code. A provider only needs two methods:

import pandas as pd
import quant_reporter as qr

class CSVProvider:
    """Reads prices from a local CSV — no network, fully reproducible."""
    def __init__(self, csv_path):
        self._prices = pd.read_csv(csv_path, index_col=0, parse_dates=True)
    def get_prices(self, tickers, start, end):
        return self._prices.loc[start:end, list(tickers)]
    def get_risk_free_rate(self):
        return 0.045

provider = CSVProvider("prices.csv")

# Per call:
qr.create_combined_report(portfolio, "SPY", "2018-01-01", "2023-12-31",
                          filename="report.html", data_provider=provider)

# …or globally, for the whole session:
qr.set_default_provider(provider)

Already have a price DataFrame in memory? qr.build_context_from_prices(prices, ...) skips fetching entirely. Pass a numeric risk_free_rate= (instead of "auto") to stay 100% offline. (Black-Litterman market caps and per-asset fundamentals are opt-in extras that still use yfinance unless your provider also implements get_market_caps.)


The report generators

All six accept the common signature above; the options below are all keyword-only and optional. Every generator also accepts data_provider= (see Data sources & offline use).

Function Focus
create_portfolio_report Risk/return dashboard vs benchmark (also aliased as create_full_report)
create_optimization_report Optimizers, sector constraints, efficient frontier, Black-Litterman
create_validation_report In-sample vs out-of-sample, overfitting score, walk-forward
create_monte_carlo_report GBM forecasting, success probabilities, stress scenarios
create_factor_report Fama-French regression + Brinson attribution
create_combined_report All of the above in a single document
import quant_reporter as qr

portfolio = {"AAPL": 0.4, "MSFT": 0.35, "GLD": 0.25}
common = dict(benchmark_ticker="SPY", train_start="2018-01-01", train_end="2023-12-31")

qr.create_portfolio_report(portfolio_dict=portfolio, filename="01_Portfolio.html", **common)
qr.create_optimization_report(portfolio_dict=portfolio, filename="02_Optimization.html", **common)
qr.create_monte_carlo_report(portfolio_dict=portfolio, filename="03_MonteCarlo.html",
                             num_simulations=5000, **common)
qr.create_validation_report(portfolio_dict=portfolio, filename="04_Validation.html", **common)
qr.create_factor_report(portfolio_dict=portfolio, filename="05_Factor.html",
                        sector_map={"AAPL": "Tech", "MSFT": "Tech", "GLD": "Commodities"},
                        **common)

Common options (keyword arguments)

Option Type Meaning
risk_free_rate float or "auto" Annual risk-free rate. "auto" fetches the live 13-week T-bill (^IRX). Default "auto".
display_names dict Friendly labels, e.g. {"AAPL": "Apple"}.
sector_map dict {ticker: sector} — enables sector constraints, sector charts, and Brinson attribution.
sector_caps / sector_mins dict {sector: max_weight} / {sector: min_weight} for the optimizer.
bl_views dict Absolute Black-Litterman views, e.g. {"AAPL": 0.15} ("AAPL returns 15% p.a.").
bl_view_confidences dict Confidence (0–1) per absolute view.
bl_relative_views list[tuple] Relative views as (outperformer, underperformer, spread), e.g. [("NVDA", "AAPL", 0.03)].
bl_relative_view_confidences list[float] Confidence (0–1) per relative view.
denoise_cov bool Eigenvalue-clip the covariance matrix before optimizing.

Black-Litterman example

qr.create_optimization_report(
    portfolio_dict={"AAPL": 0.25, "NVDA": 0.25, "JPM": 0.25, "XOM": 0.25},
    benchmark_ticker="SPY",
    train_start="2019-01-01",
    train_end="2023-12-31",
    filename="BL_Optimization.html",
    bl_views={"NVDA": 0.20},                       # absolute: NVDA returns 20% p.a.
    bl_view_confidences={"NVDA": 0.6},
    bl_relative_views=[("AAPL", "XOM", 0.05)],     # AAPL outperforms XOM by 5%
    bl_relative_view_confidences=[0.5],
)

Strategy backtesting & recommendations (2.1)

2.1 adds a first-class strategy → backtest → report loop and an opt-in recommendation layer, both additive to the 2.0 API.

Backtest a strategy

A strategy is any callable (prices, **params) -> weights — returning a dict for a static allocation or a dated DataFrame schedule — or a prebuilt from qr.REGISTRY, or a qr.Strategy wrapper. qr.backtest runs it through the cost-aware, walk-forward engine (reusing the tested simulate_strategy) and returns a rich BacktestResult.

import quant_reporter as qr

prices = qr.get_data(["SPY", "TLT", "GLD"], "2015-01-01", "2024-12-31")

res = qr.backtest(qr.risk_parity, prices, benchmark="SPY",
                  rebalance="M", cost_model=qr.transaction_cost_model)
res.metrics      # dict: CAGR, Sharpe, Sortino, Calmar, Max Drawdown, ...
res.oos_stats    # {'psr': ..., 'dsr': ...} — honest out-of-sample stats
res.report("Backtest.html", open_browser=True)   # interactive HTML report

Prebuilt strategies (keys of qr.REGISTRY): equal_weight, inverse_vol, min_variance, risk_parity, max_sharpe, trend_following, cross_sectional_momentum — plus the higher-order qr.vol_target_overlay(base_fn, target_vol=...). Schedule-producing strategies are look-ahead-safe (signals lagged, each row decided on data up to d−1).

Compare several strategies (deflated for multiple testing) in one report:

results = qr.backtest_many(
    {"EW": qr.equal_weight, "RP": qr.risk_parity, "Trend": qr.trend_following},
    prices, benchmark="SPY", cost_model=qr.transaction_cost_model)
qr.create_backtest_report(results, path="Compare.html")   # adds an OOS comparison panel

A consolidated metrics library (qr.summary_metrics, qr.sharpe, qr.sortino, qr.calmar, qr.max_drawdown, qr.value_at_risk, …) and minimize-ready objectives (qr.neg_sharpe, qr.variance, qr.cvar_objective, …) back the report and are usable on their own.

Recommendations (opt-in — the only opinionated layer)

Everything above is opinion-free with explicit parameters. The recommendation layer is where opinions live — vol target, drawdown limit, concentration caps, the selection metric — all overridable defaults. Each recommendation carries a human-readable rationale and a machine-readable evidence dict. It consumes the backtest/analytics primitives; it never re-optimizes or re-backtests.

rec = qr.recommend(
    prices,                                  # asset prices (exclude any benchmark column)
    current_weights={"SPY": 0.6, "TLT": 0.3, "GLD": 0.1},
    results=results,                         # from backtest_many — drives the verdict
    vol_target=0.10, max_drawdown_limit=0.20, max_weight=0.40,
)
rec.target_weights   # RecommendedWeights — optimal target + rationale/evidence
rec.trades           # RebalancePlan — buy/sell deltas, turnover, est. cost, no-trade band
rec.alerts           # list[RiskAlert] — vol / drawdown / concentration / sector / factor breaches
rec.verdict          # StrategyVerdict — which strategy wins on deflated Sharpe, with evidence
print(rec.to_text())                  # plain-text digest
rec.to_html("Recommendation.html")    # transparent HTML section

The four pieces are also standalone — qr.recommend_weights, qr.rebalance_trades, qr.risk_alerts, qr.compare_verdict — and a recommendation can be embedded directly into a backtest report: res.report("Backtest.html", recommendation=rec).


Library (advanced) usage

Beyond the one-call reports, the building blocks are importable for notebooks and custom scripts.

Build a context once, reuse it

from quant_reporter import build_context
from quant_reporter.optimization_report import compute_optimization_analysis

ctx = build_context({"AAPL": 0.5, "MSFT": 0.5}, "SPY", "2018-01-01", "2023-12-31")
# ctx carries price_data_full/train/test, mean_returns, cov_matrix, log_returns, ...
sections = compute_optimization_analysis(ctx)

Optimizers

from quant_reporter import (
    get_optimization_inputs, optimize_risk_parity, optimize_hrp,
    optimize_min_correlation, optimize_max_diversification,
)

mean_returns, cov_matrix, log_returns = get_optimization_inputs(price_df)
weights_rp = optimize_risk_parity(cov_matrix)
weights_hrp, _ = optimize_hrp(cov_matrix)

Fama-French factor analysis

import quant_reporter as qr

factors = qr.fetch_fama_french_factors(dataset="F-F_Research_Data_Factors_daily",
                                       start_date="2020-01-01")
res = qr.run_factor_regression(portfolio_returns, factors)   # portfolio_returns: a pd.Series
print(f"Alpha (annualized): {res['alpha']:.2%}")
print(f"Market beta: {res['betas']['Mkt-RF']:.3f}  R^2: {res['r_squared']:.3f}")

attribution = qr.compute_factor_attribution(portfolio_returns, factors,
                                            res["betas"], res["alpha"])

Brinson performance attribution

import quant_reporter as qr

# asset_returns: a DataFrame of per-asset returns (DatetimeIndex, one column per ticker)
attribution = qr.compute_brinson_attribution(
    portfolio_weights={"AAPL": 0.4, "XOM": 0.3, "JPM": 0.3},
    benchmark_weights={"AAPL": 0.3, "XOM": 0.4, "JPM": 0.2, "GS": 0.1},
    asset_returns=asset_returns,
    sector_map={"AAPL": "Tech", "XOM": "Energy", "JPM": "Finance", "GS": "Finance"},
)
print(attribution.loc["Total"])   # Allocation_Effect, Selection_Effect, Interaction_Effect, ...

Black-Litterman (low level)

from quant_reporter import calculate_black_litterman_posterior

posterior_returns, posterior_cov = calculate_black_litterman_posterior(
    hist_mean_returns, cov_matrix,
    view_dict={"AAPL": 0.10},
    relative_views=[("NVDA", "AAPL", 0.03)],   # tuples of (outperformer, underperformer, spread)
)

Monte Carlo (low level)

from quant_reporter import simulate_portfolio_paths, calculate_success_probabilities

sim = simulate_portfolio_paths(weights, mean_returns, cov_matrix,
                               num_simulations=5000, time_horizon=252)

Migrating from 1.x

2.0 unifies every report around build_context. The reports no longer take pre-computed returns/weights — they take the portfolio, benchmark, and training window and fetch data themselves:

# 1.x
qr.create_factor_report(portfolio_returns=returns, portfolio_name="Mine", filename="f.html")

# 2.0
qr.create_factor_report(portfolio_dict={"AAPL": 0.5, "MSFT": 0.5},
                        benchmark_ticker="SPY",
                        train_start="2020-01-01", train_end="2023-12-31",
                        filename="f.html")

Other changes: compute_brinson_attribution now takes a single asset_returns matrix (plus portfolio/benchmark weight dicts and a sector_map) instead of separate return series; create_full_report is retained as an alias for create_portfolio_report.


Examples & testing

  • examples/generate_all_5_reports.py — generates all five individual reports for a sample portfolio.
  • examples/example_combined_report.py — the combined flagship report.
  • examples/example_black_litterman.py — Black-Litterman views.
  • examples/example_strategy_report.py — (2.1) backtest several strategies → interactive backtest report (offline).
  • examples/example_recommendation.py — (2.1) opt-in recommendation bundle + transparent report, embedded in a backtest report (offline).
pip install -e ".[test]"
pytest            # offline unit tests; the report smoke test is skipped without network

Support & status

quant_reporter is maintained by a single author on a best-effort basis. Bug reports and PRs are welcome via GitHub Issues; responses are not guaranteed on any timeline. For security reports, see SECURITY.md. The public API follows SemVer — breaking changes bump the major version.


License

MIT — see LICENSE.


Disclaimer

Not financial advice. quant_reporter is provided for informational, research, and educational purposes only. It does not constitute financial, investment, tax, or legal advice, nor a recommendation or solicitation to buy or sell any security or financial instrument. Outputs may be inaccurate or incomplete, and all models carry assumptions and limitations (for example, the Monte Carlo engine assumes Geometric Brownian Motion, which has thin tails and understates crash risk). You are solely responsible for any decisions made using this software. Use at your own risk; the authors accept no liability for any losses.

Market-data source / Yahoo Finance terms. quant_reporter uses the third-party yfinance library (Apache-2.0) to retrieve market data from Yahoo Finance. This project is not affiliated with, endorsed by, or vetted by Yahoo, Inc. The data is intended for personal use and may be subject to Yahoo's Terms of Service. You are responsible for reviewing and complying with those terms before using any retrieved data, particularly for commercial purposes.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

quant_reporter-2.2.0.tar.gz (151.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

quant_reporter-2.2.0-py3-none-any.whl (120.9 kB view details)

Uploaded Python 3

File details

Details for the file quant_reporter-2.2.0.tar.gz.

File metadata

  • Download URL: quant_reporter-2.2.0.tar.gz
  • Upload date:
  • Size: 151.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for quant_reporter-2.2.0.tar.gz
Algorithm Hash digest
SHA256 6c0cb2db544ace45f1777620be78870c9afec914b93d3ceadc0bfbc2877420ea
MD5 e9ff256e56792b76f57961d1bedb91d4
BLAKE2b-256 d84a2d52466f4d7a508c8c495cd9673f97a7d632c3259d2c543437bda2b0ab25

See more details on using hashes here.

File details

Details for the file quant_reporter-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: quant_reporter-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 120.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for quant_reporter-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e39f75cc69b8c20f827bd2bac1a34310c724924e801f73769b071825a11a8710
MD5 63883b9a580bc94b720b5243267310de
BLAKE2b-256 18339f07f9a4e9035dff801da18a385763d379528a078037250efab2b33879c6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page