Architecture & Design¶
This is PortPy's internal design document: the full package structure, public API surface, and stage-by-stage build plan. If you just want to use PortPy, start with Getting Started instead — this page is for contributors and anyone curious how the pieces fit together, including the parts (
strategies,visualization,models.simulation) that don't exist yet.
PortPy is a Python package for quantitative portfolio analysis, performance measurement, risk assessment, and strategy backtesting. The design centers on a single Portfolio object that encapsulates all data (prices/returns and weights) and provides a unified interface for metrics, visualization, models, and strategies. This document defines the package's structure, core components, public API, and development roadmap.
1. Design Philosophy¶
- Single entry point: The user interacts primarily with the
Portfolioclass. All functionalities are accessible through its methods (or namespaced attributes like.metrics,.models,.visualization). - Pure functions under the hood: Metrics and models are implemented as standalone functions that take price/return series (or a fitted result) as input and return numeric results, structured results, or
MetricResult/ModelResultobjects. ThePortfolionamespaces are thin wrappers that pass internal data. - Data autonomy: The package does not fetch or clean data from external sources. The user provides a well-formed pandas
DataFrame(or a dictionary of assets) containing prices or returns. ThePortfolioconstructor validates basic structure and converts to a canonical internal format. - Modular but unified: Internal code is organized into logical subpackages (
metrics,visualization,models,strategies), but the public API is flattened through thePortfolioclass for discoverability and ease of use. - Extensibility: New metrics, models, or strategies can be added by implementing the required interfaces (function signatures) and registering them in the appropriate registry.
2. Package Structure¶
The package uses the src/ layout with pyproject.toml as the build system (Hatchling). Dependencies are minimal: numpy, pandas, scipy, statsmodels among a few others. Visualization extras (plotly, matplotlib) and ML extras (scikit-learn, arch, hmmlearn) are declared as optional extras ([viz], [models], [all]).
portpy/
├── src/
│ └── portpy/
│ ├── __init__.py
│ ├── portfolio.py
│ ├── explain.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── asset.py
│ │ ├── calendar.py
│ │ ├── currency.py
│ │ └── weights.py
│ ├── metrics/
│ │ ├── __init__.py
│ │ ├── benchmarks.py
│ │ ├── costs.py
│ │ ├── covariance.py
│ │ ├── distributions.py
│ │ ├── drawdowns.py
│ │ ├── performance.py
│ │ ├── returns.py
│ │ ├── risk.py
│ │ ├── rolling.py
│ │ └── summary.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── base.py # ModelResult, constraint objects, the shared SLSQP solve routine
│ │ ├── estimators/ # expected_returns, covariance, factor_models (CAPM/Fama-French/OLS)
│ │ ├── optimization/ # pure solvers: mean_variance, max_sharpe, HRP, risk parity, Black-Litterman, ...
│ │ ├── construction/ # constraint re-exports + optimize/build/efficient_frontier
│ │ ├── management/ # rebalance, monitor, compare
│ │ └── simulation/ # Not yet implemented - stochastic processes, Monte Carlo, time series, ML
│ ├── strategies/ # Not yet implemented
│ │ ├── base/
│ │ ├── passive/
│ │ ├── momentum/
│ │ ├── mean_reversion/
│ │ ├── trend_following/
│ │ ├── volatility_targeting/
│ │ ├── factor_tilt/
│ │ ├── sector_rotation/
│ │ └── evaluation/
│ ├── visualization/ # Not yet implemented
│ │ ├── charts.py
│ │ ├── dashboards.py
│ │ ├── export.py
│ │ └── themes.py
│ └── utils/
│ ├── constants.py
│ └── validation.py
├── tests/
├── docs/
├── examples/
├── pyproject.toml
Key points:
portfolio.pydefines thePortfolioclass. It builds every one of.metrics/.models(and, eventually,.visualization) on a shared_AutoFillNamespacemixin, auto-filling arguments (returns,prices,weights,expected_returns,cov_matrix,current_weights,rf,periods_per_year, ...) from the portfolio's own state where possible.metrics/contains pure functions; each submodule groups related metrics (e.g.returns.py). Every function also works standalone against a plain pandas Series/DataFrame.explain.pyis the explainability layer: a registry ofExplanationcards plusMetricResult, a float subclass that knows its own name and can render a full explanation on demand.models/base.py'sModelResultmirrors it for structured (non-scalar) results.models/scopes to building and running a portfolio:estimatorsfor inputs (expected returns/covariance/factor models),optimizationfor pure solver functions,constructionfor the constraint objects and theoptimize/build/efficient_frontierrecipe layer,managementfor what happens after (rebalancing, drift monitoring, comparisons).models/simulation/(research tooling - stochastic simulation, forecasting, ML) is a later, separate stage.visualization/will provide plotting functions that acceptPortfoliodata or pre-computed metric/model results.strategies/will implement trading strategies and a backtesting engine.
3. Data Layer (User-Provided)¶
The user is responsible for acquiring and cleaning their data. The Portfolio constructor expects a pandas DataFrame in one of two formats:
- Price format: Columns = asset symbols, index = datetime (daily or lower frequency). Prices should be adjusted for splits/dividends.
- Return format: Same shape, but values are periodic (daily) returns (simple or log). If returns are provided, the package still reconstructs cumulative prices from an initial value internally.
Validation performed inside Portfolio:
- Index is
DatetimeIndex, sorted, and duplicate-free (raises otherwise — PortPy never silently reorders or deduplicates). - Minimal coverage: at least two observations.
- Missing values are allowed structurally, but each metric function does its own
dropna()and raises if too few observations remain. - An optional
weightsvector (Series/dict/array, negative values allowed for shorts) can be provided; defaults to equal weight (1/N). - Calendar alignment: mixed assets (e.g. 24/7 crypto and Mon-Fri equities) must be aligned by the user before constructing a
Portfolio— seeportpy.core.align_calendars. PortPy never forward-fills or drops rows on your behalf. - Currency alignment: all assets must be denominated in the same base currency.
portpy.core.convert_to_base_currencyis provided as an explicit, opt-in helper (with an optionalmax_stalenessto cap how long a forward-filled FX rate may be carried) — PortPy performs no FX conversion implicitly.Portfolio(..., base_currency=...)records that currency as a label only, likeasset_classes.portpy.core.calculate_fx_spreadturns real bid/ask FX quotes into a spread estimate formetrics.costs.fx_spread_costs.
No data fetching, caching, or cleaning is included. portpy.core and portpy.utils.validation provide helpers the user may call before constructing a Portfolio, but they are never invoked automatically.
4. The Portfolio Class¶
The Portfolio class is the central object. It holds:
prices(orreturns) as a DataFrameweights(Series, normalized to sum to 1, default 1/N, negative values allowed)name(optional, default"My Portfolio")frequency(trading periods per year, used to annualize metrics — default 252)risk_free_rate(annual rate, default 0.0)asset_classes(optional{symbol: AssetClass}tags, purely for reporting/grouping)base_currency(optional, defaultNone— a label only, likeasset_classes; never used to convert anything)
4.1 Basic Accessors¶
.prices: price DataFrame..returns(period=1, log=False): the portfolio's own aggregated return series (weights applied)..asset_returns(log=False): per-asset return DataFrame (not weight-aggregated)..price_index(base=100.0): synthetic portfolio value series, rebased..asset_names,.num_assets,.weights..set_weights(weights),.set_risk_free_rate(rate).
4.2 Metrics (via .metrics)¶
Every function in portpy.metrics is available as portfolio.metrics.<name>(...). Parameters named returns, y, prices, weights, or cov_matrix are auto-filled from the portfolio when the call uses only keyword arguments; rf and periods_per_year default to the portfolio's own risk_free_rate and frequency. See the API reference for the full list, grouped by submodule (returns, risk, performance, drawdowns, rolling, distributions, benchmarks, covariance, summary, costs).
4.3 Visualization (via .visualization) — not yet implemented¶
Planned: methods generating Plotly/Matplotlib figures (price_chart, drawdown_chart, correlation_heatmap, a full tearsheet(), etc.), each carrying its own .explain().
4.4 Models (via .models)¶
A namespace of namespaces, each reusing the same _AutoFillNamespace mixin as .metrics:
.models.estimators—expected_returns(method="mean_historical"|"ewma"|"capm_implied"|"james_stein"),covariance(method="sample"|"ewma"|"shrinkage"|"ledoit_wolf"|"robust"), andfactor_models(capm,fama_french,linear_regression,rolling_regression,factor_attribution) built onstatsmodels..models.optimization— pure solver functions,(expected_returns, cov_matrix, constraints) -> weights:mean_variance,max_sharpe,min_variance,target_return,target_volatility,efficient_frontier,black_litterman,hierarchical_risk_parity,risk_parity,risk_budgeting,maximum_diversification. Solved via multi-start SLSQP (scipy.optimize, no cvxpy dependency) — seemodels/base.py..models.construction— constraint objects (WeightBounds,GroupCap,TurnoverCap,NetExposure,GrossExposure), plusoptimize/build/efficient_frontier(also exposed directly on.modelsfor convenience — the recipe layer most users hit)..models.management—rebalance(trade list from current to target weights),monitor(drift/exposure/limit checks),compare(before/after or portfolio-vs-portfolio).
Every optimize/build/efficient_frontier/rebalance/compare call returns a ModelResult (models/base.py), carrying .explain(), .summary(), .plot() (raises NotImplementedError until .visualization ships), and .compare(). See the models guide and API reference.
4.5 Strategies (via .strategies) — not yet implemented¶
Planned: a BaseStrategy interface, a backtest() engine, and concrete strategies (buy-and-hold, momentum, mean-reversion, pairs trading, vol targeting, sector rotation, etc.).
5. Explainability (portpy.explain)¶
Every metric can explain itself. Explanation is a structured knowledge card (summary, formula, how-to-read, good-vs-bad, caveats, and an optional interpret function that turns a live value into a one-line verdict), registered per-function at the bottom of the module that defines it. MetricResult is a float subclass carrying its own name, unit, and .explain() method. portpy.explain(name_or_result) is the single dispatch point — see The Explainability Layer.
6. Stage Plan¶
Stage 1 — Metrics & Core — DONE¶
All of portpy.metrics (returns, risk, performance, drawdowns, rolling, distributions, benchmarks, covariance, summary, costs) and portpy.core (asset tagging, calendar alignment, currency conversion, weight normalization), each with full type hints, docstrings, and a registered Explanation.
Stage 2 — Visualization — not started¶
visualization/charts.py, dashboards.py, export.py, themes.py. Plotly-first, one chart function per metric family, plus a full tearsheet() and a comparison_dashboard().
Stage 3 — Portfolio Construction, Optimization & Management — DONE¶
portpy.models.estimators (expected returns, covariance, factor models), .optimization (mean-variance, max Sharpe, min variance, target return/volatility, efficient frontier, Black-Litterman, hierarchical risk parity, risk parity/budgeting, maximum diversification — solved via multi-start SLSQP, no cvxpy dependency), .construction (constraint objects + the optimize/build/efficient_frontier recipe layer), and .management (rebalance/monitor/compare). Cross-validated against PyPortfolioOpt, Riskfolio-Lib, and skfolio — see Testing Strategy below.
Stage 4 — Simulation & Forecasting Toolkit — not started¶
portpy.models.simulation: stochastic process simulation/calibration (GBM, jump-diffusion, ...), Monte Carlo (VaR/CVaR, bootstrap, path statistics), classical time series (ARIMA/GARCH), and a small ML toolkit (PCA factors, regime detection). Doesn't block Stage 3 and shouldn't hold up shipping it.
Stage 5 — Strategies — not started¶
BaseStrategy, a backtest() engine, position sizing (Kelly, vol targeting), a library of concrete strategies (buy-and-hold, momentum variants, mean-reversion, pairs trading, breakout, vol targeting, factor tilt, sector rotation), and evaluation tooling (walk-forward analysis, Monte Carlo robustness).
See Roadmap for the current, plain-language version of this table.
7. Testing Strategy¶
- Unit tests: every metric/model function tested against known-input/known-output fixtures.
- Fixtures: synthetic constant-return series (edge cases), seeded Normal and fat-tailed (Student-t) series (approximate checks against known moments), business-day and 7-day calendars.
- Cross-validation:
tests/validation/test_vs_empyrical_quantstats.pychecks PortPy's metrics againstempyricalandquantstats;tests/validation/test_models_vs_libraries.pychecks every optimizer/estimator againstPyPortfolioOpt,Riskfolio-Lib, and/orskfolio(a third, independent convex-optimization stack — planning.md's "at least 3 other libraries" bar; plus a plain-numpy OLS closed form for the factor regressions, and explicit checks for negative-weight/short-position solutions, not just long-only) — both on synthetic data (always) and real yfinance data (@pytest.mark.network, opt-in). - Portfolio integration tests:
Portfoliomethods correctly delegate to functions and handle multi-asset DataFrames, including negative (short) weights. - CI: GitHub Actions runs lint (
ruff), type checking (mypy), and the non-network test suite on Python 3.10–3.12.
8. Package Metadata¶
- Name:
portpy-quant(import nameportpy). - Dependencies:
numpy,pandas,scipy,statsmodels. - Optional extras:
viz(plotly, matplotlib, seaborn, kaleido),models(scikit-learn, arch, hmmlearn),data(yfinance, alpaca-py, python-dotenv),all,dev(addsPyPortfolioOpt/Riskfolio-Lib/skfolioalongsideempyrical/quantstats, for cross-validation only — never a runtime dependency of the library itself). - Build backend:
hatchling.
See CHANGELOG for release history.