Architecture¶
A short tour of how jQuantStats is put together, for contributors.
Two entry points, one analytics pipeline¶
prices + positions ──► Portfolio ──► .data ──► Data ◄── returns series
│ │
│ ├── .stats (Stats facade)
├── .stats ───────────┤
├── .plots (PortfolioPlots) ├── .plots (DataPlots)
├── .report (Report) ├── .reports (Reports)
└── .utils (PortfolioUtils) └── .utils (DataUtils)
Portfolio(portfolio.py) starts from raw inputs — prices, cash positions, AUM — and compiles the NAV/returns chain. Use it when you know what you held.Data(data.py) starts from a returns series (plus optional benchmark and risk-free rate). Use it when you only know what you made.- The bridge is one-directional:
portfolio.dataproduces aDataobject from the portfolio's daily returns, so every returns-series analytic is always available from a Portfolio.Portfolio.plots/reportare deliberately not delegated — they exploit raw prices/positions that a bare returns series doesn't have.
Mixin composition¶
Both core classes stay small by composing focused mixins:
Portfolio=PortfolioNavMixin(NAV & returns chain) +PortfolioAttributionMixin(tilt/timing) +PortfolioTurnoverMixin+PortfolioCostMixin, declared in_portfolio_*.pymodules.Stats(_stats/_stats.py) composes_core,_basic,_performance,_reporting,_rolling, and_montecarlomixins — roughly: primitives, distribution/risk metrics, Sharpe-family metrics, summary/report tables, rolling windows, and Monte Carlo simulation.
Each mixin declares the attributes it expects from the composed class inside
an if TYPE_CHECKING: block, so it type-checks standalone without importing
the concrete class.
Facades and lazy composition¶
Data and Portfolio expose analytics through lazy accessor properties
(.stats, .plots, .reports/.report, .utils) that construct a facade
object on first use. On Portfolio (a frozen, slotted dataclass) the results
are memoised in declared slot fields via cached_in_slot (_cache.py) —
functools.cached_property can't be used because slots leave no __dict__,
and plain assignment is blocked by frozen=True.
Protocol layering¶
The analytics subpackages (_stats, _plots, _reports, _utils) never
import the concrete Data/Portfolio classes at runtime — that would be
circular, since those classes compose the subpackages. Instead:
_protocol.py(root) defines the single sharedDataLikeandStatsLikestructural protocols.- Each subpackage defines its own minimal
PortfolioLike(_plots/_protocol.py,_reports/_protocol.py,_utils/_protocol.py) listing only the members it consumes — interface segregation, kept deliberately un-merged so subpackages don't re-couple to the full Portfolio surface.
Where things live¶
| Concern | Location |
|---|---|
| Public API surface | __init__.py (Portfolio, Data, CostModel, Result, interpolate) |
| Input validation & domain errors | exceptions.py, validated in __post_init__/factories |
| Cost models (per-unit vs turnover-bps) | _cost_model.py, applied in _portfolio_cost.py |
| HTML reports | _reports/ + templates/portfolio_report.html |
Web API (optional [web] extra) |
api/app.py |
| Quality gates | 100% line+branch coverage, 100% docstring coverage, ruff, ty, mutation gate (bin/mutation_gate.py) |