Skip to content

API Reference

The public API surface of jquantstats. All stable exports are importable directly from the top-level package:

from jquantstats import Portfolio, Data, CostModel
Export What it is Start here when…
Portfolio Position-level analytics: NAV, turnover, costs, attribution, lag() you have prices and positions
Data Returns-level analytics: 50+ stats, plots, HTML report you have returns (or prices only)
CostModel Declarative trading-cost specification (per-unit or turnover-bps) you want cost-adjusted analytics
Result Lightweight container returned by some report helpers you consume report internals
NativeFrame, NativeFrameOrScalar Type aliases for narwhals-compatible input frames you type-annotate your own code

Exceptions live in jquantstats.exceptions and all inherit from JQuantStatsError, so except JQuantStatsError catches the whole family.

See API Stability for the versioning and deprecation policy, and the FAQ for common errors.


Portfolio

jquantstats.Portfolio dataclass

Bases: PortfolioNavMixin, PortfolioAttributionMixin, PortfolioTurnoverMixin, PortfolioCostMixin, PortfolioTransformMixin, PortfolioUnitsMixin, PortfolioConstructorMixin

Portfolio analytics class for quant finance.

Stores the three raw inputs — cash positions, prices, and AUM — and exposes the standard derived data series, analytics facades, transforms, and attribution tools.

Derived data series:

  • profits — per-asset daily cash P&L
  • profit — aggregate daily portfolio profit
  • nav_accumulated — cumulative additive NAV
  • nav_compounded — compounded NAV
  • returns — daily returns (profit / AUM)
  • monthly — monthly compounded returns
  • highwater — running high-water mark
  • drawdown — drawdown from high-water mark
  • all — merged view of all derived series

  • Lazy composition accessors: stats, plots, report, data, as_data (the same bridge over a derived returns frame)

  • Portfolio transforms: truncate, lag, smoothed_holding
  • Attribution: tilt, timing, tilt_timing_decomp
  • Turnover: turnover, turnover_weekly, turnover_summary
  • Share-count view: units, equity, trades_units, trades_currency, weights — all derived from cash positions and prices, so they are available however the portfolio was constructed
  • Cost analysis: cost_adjusted_returns, trading_cost_impact, deduct_management_fee
  • Utility: correlation

Attributes:

Name Type Description
cashposition DataFrame

Polars DataFrame of positions per asset over time. Any temporal column is present as 'date' — see Date column below.

prices DataFrame

Polars DataFrame of prices per asset over time. Any temporal column is present as 'date' — see Date column below.

aum float

Assets under management used as base NAV offset.

Analytics facades

  • .stats : delegates to the legacy Stats pipeline via .data; all 50+ metrics available.
  • .plots : portfolio-specific Plots; NAV overlays, lead-lag IR, rolling Sharpe/vol, heatmaps.
  • .report : HTML Report; self-contained portfolio performance report.
  • .data : bridge to the legacy Data / Stats / DataPlots pipeline.
  • .as_data(frame) : the same bridge over a derived returns frame — cost-adjusted, fee-deducted, or hand-built. Prefer it over Data.from_returns(frame), which would also treat the 'profit' and 'NAV_accumulated' columns as assets.

.plots and .report are intentionally not delegated to the legacy path: the legacy path operates on a bare returns series, while the analytics path has access to raw prices, positions, and AUM for richer portfolio-specific visualisations.

Cost models

Two independent cost models are provided. They are not interchangeable:

Model A — position-delta (stateful, set at construction): cost_per_unit: float — one-way cost per unit of position change (e.g. 0.01 per share). Used by .position_delta_costs and .net_cost_nav. Best for: equity portfolios where cost scales with shares traded.

Model B — turnover-bps (stateless, passed at call time): cost_bps: float — one-way cost in basis points of AUM turnover (e.g. 5 bps). Used by .cost_adjusted_returns(cost_bps) and .trading_cost_impact(max_bps). Best for: macro / fund-of-funds portfolios where cost scales with notional traded.

Management fee (flat annual, set at construction or passed at call time): annual_fee: float — flat annual management fee as a fraction of AUM (e.g. 0.0085 for 85 bps p.a.). Used by .deduct_management_fee(annual_fee). The fee accrues pro-rata per calendar day (annual_fee * days / 365), so weekends and holidays are charged to the next trading day and the deduction sums to annual_fee over a full year. Cost and fee deductions compose linearly in any order.

To sweep a range of cost assumptions use trading_cost_impact(max_bps=20) (Model B). To compute a net-NAV curve set cost_per_unit at construction and read .net_cost_nav (Model A).

Date column

The date axis is identified internally by the name date, so a temporal column (pl.Date or pl.Datetime) under any other label — 'Date', 'timestamp' — is renamed to date at construction. prices and cashposition therefore report date rather than the caller's original name, and every date-dependent feature works regardless of what the input column was called.

Only genuinely temporal columns are normalised: a date column still held as strings is left as-is and the portfolio is treated as integer-indexed. Parse it first (pl.col("Date").str.to_date()) to get a temporal axis. When a frame contains several temporal columns and none is named date, the first is renamed.

Most analytics work with or without a date column. The following features require a temporal date column:

  • portfolio.plots.correlation_heatmap()
  • portfolio.plots.lead_lag_ir_plot()
  • stats.monthly_win_rate() — returns NaN per column when no date is present
  • stats.annual_breakdown() — raises ValueError when no date is present
  • stats.max_drawdown_duration() — returns period count (int) instead of days

Portfolios without a date column (integer-indexed) are fully supported for NAV, returns, Sharpe, drawdown, cost analytics, and most rolling metrics.

Examples:

>>> import polars as pl
>>> from datetime import date
>>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
>>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
>>> pf.assets
['A']
Source code in src/jquantstats/portfolio.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
@dataclasses.dataclass(frozen=True, slots=True)
class Portfolio(
    PortfolioNavMixin,
    PortfolioAttributionMixin,
    PortfolioTurnoverMixin,
    PortfolioCostMixin,
    PortfolioTransformMixin,
    PortfolioUnitsMixin,
    PortfolioConstructorMixin,
):
    """Portfolio analytics class for quant finance.

    Stores the three raw inputs — cash positions, prices, and AUM — and
    exposes the standard derived data series, analytics facades, transforms,
    and attribution tools.

    Derived data series:

    - `profits` — per-asset daily cash P&L
    - `profit` — aggregate daily portfolio profit
    - `nav_accumulated` — cumulative additive NAV
    - `nav_compounded` — compounded NAV
    - `returns` — daily returns (profit / AUM)
    - `monthly` — monthly compounded returns
    - `highwater` — running high-water mark
    - `drawdown` — drawdown from high-water mark
    - `all` — merged view of all derived series

    - Lazy composition accessors: `stats`, `plots`, `report`, `data`,
      `as_data` (the same bridge over a derived returns frame)
    - Portfolio transforms: `truncate`, `lag`,
      `smoothed_holding`
    - Attribution: `tilt`, `timing`, `tilt_timing_decomp`
    - Turnover: `turnover`, `turnover_weekly`,
      `turnover_summary`
    - Share-count view: `units`, `equity`, `trades_units`,
      `trades_currency`, `weights` — all derived from cash positions and
      prices, so they are available however the portfolio was constructed
    - Cost analysis: `cost_adjusted_returns`,
      `trading_cost_impact`, `deduct_management_fee`
    - Utility: `correlation`

    Attributes:
        cashposition: Polars DataFrame of positions per asset over time.  Any
            temporal column is present as ``'date'`` — see *Date column* below.
        prices: Polars DataFrame of prices per asset over time.  Any temporal
            column is present as ``'date'`` — see *Date column* below.
        aum: Assets under management used as base NAV offset.

    Analytics facades
    -----------------
    - ``.stats``   : delegates to the legacy ``Stats`` pipeline via ``.data``; all 50+ metrics available.
    - ``.plots``   : portfolio-specific ``Plots``; NAV overlays, lead-lag IR, rolling Sharpe/vol, heatmaps.
    - ``.report``  : HTML ``Report``; self-contained portfolio performance report.
    - ``.data``    : bridge to the legacy ``Data`` / ``Stats`` / ``DataPlots`` pipeline.
    - ``.as_data(frame)`` : the same bridge over a *derived* returns frame — cost-adjusted,
      fee-deducted, or hand-built. Prefer it over ``Data.from_returns(frame)``, which would
      also treat the ``'profit'`` and ``'NAV_accumulated'`` columns as assets.

    ``.plots`` and ``.report`` are intentionally *not* delegated to the legacy path: the legacy
    path operates on a bare returns series, while the analytics path has access to raw prices,
    positions, and AUM for richer portfolio-specific visualisations.

    Cost models
    -----------
    Two independent cost models are provided. They are not interchangeable:

    **Model A — position-delta (stateful, set at construction):**
        ``cost_per_unit: float``  — one-way cost per unit of position change (e.g. 0.01 per share).
        Used by ``.position_delta_costs`` and ``.net_cost_nav``.
        Best for: equity portfolios where cost scales with shares traded.

    **Model B — turnover-bps (stateless, passed at call time):**
        ``cost_bps: float``  — one-way cost in basis points of AUM turnover (e.g. 5 bps).
        Used by ``.cost_adjusted_returns(cost_bps)`` and ``.trading_cost_impact(max_bps)``.
        Best for: macro / fund-of-funds portfolios where cost scales with notional traded.

    **Management fee (flat annual, set at construction or passed at call time):**
        ``annual_fee: float``  — flat annual management fee as a fraction of AUM (e.g. 0.0085 for 85 bps p.a.).
        Used by ``.deduct_management_fee(annual_fee)``.
        The fee accrues pro-rata per calendar day (``annual_fee * days / 365``), so weekends and
        holidays are charged to the next trading day and the deduction sums to ``annual_fee`` over a
        full year.  Cost and fee deductions compose linearly in any order.

    To sweep a range of cost assumptions use ``trading_cost_impact(max_bps=20)`` (Model B).
    To compute a net-NAV curve set ``cost_per_unit`` at construction and read ``.net_cost_nav`` (Model A).

    Date column
    -----------
    The date axis is identified internally by the name ``date``, so a temporal column
    (``pl.Date`` or ``pl.Datetime``) under any other label — ``'Date'``, ``'timestamp'`` —
    is **renamed to ``date`` at construction**.  ``prices`` and ``cashposition`` therefore
    report ``date`` rather than the caller's original name, and every date-dependent
    feature works regardless of what the input column was called.

    Only genuinely temporal columns are normalised: a date column still held as strings
    is left as-is and the portfolio is treated as integer-indexed.  Parse it first
    (``pl.col("Date").str.to_date()``) to get a temporal axis.  When a frame contains
    several temporal columns and none is named ``date``, the first is renamed.

    Most analytics work with or without a date column. The following features require a
    temporal ``date`` column:

    - ``portfolio.plots.correlation_heatmap()``
    - ``portfolio.plots.lead_lag_ir_plot()``
    - ``stats.monthly_win_rate()``      — returns NaN per column when no date is present
    - ``stats.annual_breakdown()``      — raises ``ValueError`` when no date is present
    - ``stats.max_drawdown_duration()`` — returns period count (int) instead of days

    Portfolios without a ``date`` column (integer-indexed) are fully supported for
    NAV, returns, Sharpe, drawdown, cost analytics, and most rolling metrics.

    Examples:
        >>> import polars as pl
        >>> from datetime import date
        >>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
        >>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
        >>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
        >>> pf.assets
        ['A']
    """

    cashposition: pl.DataFrame
    prices: pl.DataFrame
    aum: float
    cost_per_unit: float = 0.0
    cost_bps: float = 0.0
    annual_fee: float = 0.0

    # ── Internal cache fields ─────────────────────────────────────────────────
    # All cache fields are initialised to ``None`` in ``__post_init__`` via
    # ``object.__setattr__`` (required for frozen dataclasses) and populated
    # lazily on first property access.
    #
    # Lifecycle:
    #   - Initialised: ``__post_init__`` sets every field to ``None``.
    #   - Populated: each property computes its value on the first call and
    #     writes it back via ``object.__setattr__``.
    #   - Invalidation: not required — ``Portfolio`` is a *frozen* dataclass,
    #     so its inputs never change and all derived values remain valid for the
    #     lifetime of the instance.
    _data_bridge: "Data | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)
    _stats_cache: "Stats | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)
    _plots_cache: "PortfolioPlots | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)
    _report_cache: "Report | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)
    _utils_cache: "PortfolioUtils | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)
    _profits_cache: "pl.DataFrame | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)
    _returns_cache: "pl.DataFrame | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)
    _tilt_cache: "Portfolio | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)
    _turnover_cache: "pl.DataFrame | None" = dataclasses.field(init=False, repr=False, compare=False, hash=False)

    @staticmethod
    def _build_data_bridge(ret: pl.DataFrame) -> "Data":
        """Build a `Data` bridge from a returns frame.

        Narrows *ret* to its ``'returns'`` column and splits out ``'date'`` (if
        present) into the index.  The narrowing is the whole point: the
        portfolio's returns-shaped frames also carry ``'profit'`` and
        ``'NAV_accumulated'``, and passing those to `Data` would have it treat a
        cash P&L series and a NAV level as two further "assets".

        The date column is matched literally after a
        `_normalise_date_column` pass, so a caller-supplied frame whose dates
        arrived as ``'Date'`` is handled too; the positional-index fallback is
        reached only by a frame that genuinely has no temporal column.

        Args:
            ret: Returns DataFrame with a ``'returns'`` column, optionally with
                a date column and any number of columns to ignore.

        Returns:
            A `Data` instance backed by the ``'returns'`` column of *ret*.

        Raises:
            MissingReturnsColumnError: If *ret* has no ``'returns'`` column.
        """
        if "returns" not in ret.columns:
            raise MissingReturnsColumnError(ret.columns)
        ret = _normalise_date_column(ret)
        returns_only = ret.select("returns")
        if _DATE_COLUMN in ret.columns:
            return Data(returns=returns_only, index=ret.select(_DATE_COLUMN))
        return Data(returns=returns_only, index=pl.DataFrame({"index": list(range(ret.height))}))

    def __post_init__(self) -> None:
        """Validate input types, shapes, and parameters, and normalise the date column."""
        if not isinstance(self.prices, pl.DataFrame):
            raise InvalidPricesTypeError(type(self.prices).__name__)
        if not isinstance(self.cashposition, pl.DataFrame):
            raise InvalidCashPositionTypeError(type(self.cashposition).__name__)
        # Canonicalise the date axis before anything downstream looks for it; the
        # mixins match ``'date'`` by name, so this must happen on every
        # construction path, including a direct ``Portfolio(...)`` call.
        object.__setattr__(self, "prices", _normalise_date_column(self.prices))
        object.__setattr__(self, "cashposition", _normalise_date_column(self.cashposition))
        if self.cashposition.shape[0] != self.prices.shape[0]:
            raise RowCountMismatchError(self.prices.shape[0], self.cashposition.shape[0])
        if self.aum <= 0.0:
            raise NonPositiveAumError(self.aum)
        for slot in _CACHE_SLOTS:
            object.__setattr__(self, slot, None)

    def _date_range(self) -> tuple[int, date | datetime | None, date | datetime | None]:
        """Return (rows, start, end) for the portfolio's returns series.

        ``start`` and ``end`` are ``None`` when there is no ``'date'`` column.
        """
        ret = self.returns
        rows = ret.height
        if "date" in ret.columns:
            return rows, cast(date | None, ret["date"].min()), cast(date | None, ret["date"].max())
        return rows, None, None

    @property
    def cost_model(self) -> CostModel:
        """Return the active cost model as a `CostModel` instance.

        Returns:
            A `CostModel` whose ``cost_per_unit`` and ``cost_bps`` fields
            reflect the values stored on this portfolio.
        """
        return CostModel(cost_per_unit=self.cost_per_unit, cost_bps=self.cost_bps)

    def __repr__(self) -> str:
        """Return a string representation of the Portfolio object."""
        rows, start, end = self._date_range()
        if start is not None:
            return f"Portfolio(assets={self.assets}, rows={rows}, start={start}, end={end})"
        return f"Portfolio(assets={self.assets}, rows={rows})"

    def describe(self) -> pl.DataFrame:
        """Return a tidy summary of shape, date range and asset names.

        Returns:
        -------
        pl.DataFrame
            One row per asset with columns: asset, start, end, rows.

        Examples:
            >>> import polars as pl
            >>> from datetime import date
            >>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
            >>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
            >>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
            >>> df = pf.describe()
            >>> list(df.columns)
            ['asset', 'start', 'end', 'rows']
        """
        rows, start, end = self._date_range()
        return pl.DataFrame(
            {
                "asset": self.assets,
                "start": [start] * len(self.assets),
                "end": [end] * len(self.assets),
                "rows": [rows] * len(self.assets),
            }
        )

    # ── Factory classmethods ──────────────────────────────────────────────────

    @classmethod
    def from_cash_position(
        cls,
        prices: pl.DataFrame,
        cash_position: pl.DataFrame | pl.Expr,
        aum: float,
        cost_per_unit: float = 0.0,
        cost_bps: float = 0.0,
        cost_model: CostModel | None = None,
        annual_fee: float = 0.0,
    ) -> Self:
        """Create a Portfolio directly from cash positions aligned with prices.

        Args:
            prices: Price levels per asset over time.  A temporal column is
                normalised to ``'date'`` at construction, whatever it was named.
            cash_position: Cash exposure per asset over time, either as a
                DataFrame or as a Polars expression evaluated against *prices*.
            aum: Assets under management used as the base NAV offset.
            cost_per_unit: One-way trading cost per unit of position change.
                Defaults to 0.0 (no cost).  Ignored when *cost_model* is given.
            cost_bps: One-way trading cost in basis points of AUM turnover.
                Defaults to 0.0 (no cost).  Ignored when *cost_model* is given.
            cost_model: Optional `CostModel`
                instance.  When supplied, its ``cost_per_unit`` and
                ``cost_bps`` values take precedence over the individual
                parameters above.
            annual_fee: Flat annual management fee as a fraction of AUM
                (e.g. 0.0085 for 85 bps p.a.).  Defaults to 0.0 (no fee).
                Used as the default by `deduct_management_fee`.

        Returns:
            A Portfolio instance with the provided cash positions.

        Raises:
            PositionExprColumnError: If *cash_position* is an expression that
                creates columns not present in *prices* (e.g. via ``.alias``);
                such expressions leave the original asset columns untouched,
                silently treating raw prices as positions.
        """
        if isinstance(cash_position, pl.Expr):
            cash_position = _evaluate_position_expr(prices, cash_position, "cash_position")
        if cost_model is not None:
            cost_per_unit = cost_model.cost_per_unit
            cost_bps = cost_model.cost_bps
        return cls(
            prices=prices,
            cashposition=cash_position,
            aum=aum,
            cost_per_unit=cost_per_unit,
            cost_bps=cost_bps,
            annual_fee=annual_fee,
        )

    # ── Internal helpers ───────────────────────────────────────────────────────

    @staticmethod
    def _assert_clean_series(series: pl.Series, name: str = "") -> None:
        """Raise `UncleanSeriesError` if *series* contains nulls or non-finite values.

        Args:
            series: The series to validate.
            name: Optional series name included in the error message.

        Raises:
            UncleanSeriesError: If the series contains null or non-finite values.
        """
        if series.null_count() != 0:
            raise UncleanSeriesError(name, "null")
        if not series.is_finite().all():
            raise UncleanSeriesError(name, "non-finite")

    # ── Core data properties ───────────────────────────────────────────────────

    @property
    def assets(self) -> list[str]:
        """List the asset column names from prices (numeric columns).

        Returns:
            list[str]: Names of numeric columns in prices; typically excludes
            ``'date'``.
        """
        return [c for c in self.prices.columns if self.prices[c].dtype.is_numeric()]

    # ── Lazy composition accessors ─────────────────────────────────────────────

    @property
    @cached_in_slot("_data_bridge")
    def data(self) -> "Data":
        """Build a legacy `Data` object from this portfolio's returns.

        This bridges the two entry points: ``Portfolio`` compiles the NAV curve from
        prices and positions; the returned `Data` object
        gives access to the full legacy analytics pipeline (``data.stats``,
        ``data.plots``, ``data.reports``).

        Returns:
            `Data`: A Data object whose ``returns`` column
            is the portfolio's daily return series and whose ``index`` holds the date
            column (or a synthetic integer index for date-free portfolios).

        Examples:
            >>> import polars as pl
            >>> from datetime import date
            >>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
            >>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
            >>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
            >>> d = pf.data
            >>> "returns" in d.returns.columns
            True
        """
        return self.as_data()

    def as_data(self, returns: pl.DataFrame | None = None) -> "Data":
        """Bridge a returns-shaped frame into a `Data` object.

        `data` is the same bridge hardwired to `returns`.  Use this
        method when the series you want analysed is a *derived* one — the output
        of `cost_adjusted_returns`, `deduct_management_fee`, or any frame you
        built yourself — so it reaches `Stats` through the same narrowing the
        `data` property uses.

        Building the `Data` by hand is the trap this exists to close:
        the portfolio's returns-shaped frames carry ``'profit'`` and
        ``'NAV_accumulated'`` alongside ``'returns'``, and
        ``Data.from_returns(pf.returns)`` therefore reports a Sharpe ratio for
        the cash P&L series and the NAV level as if they were two more assets.
        This method keeps only ``'returns'``.

        Args:
            returns: Frame with a ``'returns'`` column, optionally a date
                column, and any number of other columns (which are ignored).
                Defaults to `returns`.

        Returns:
            `Data`: A Data object over the single return series, indexed by the
            frame's date column when it has one and by row position otherwise.

        Raises:
            MissingReturnsColumnError: If *returns* has no ``'returns'`` column.

        Examples:
            >>> import polars as pl
            >>> from datetime import date
            >>> _d = [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)]
            >>> prices = pl.DataFrame({"date": _d, "A": [100.0, 110.0, 121.0]})
            >>> pos = pl.DataFrame({"date": _d, "A": [1000.0, 1000.0, 1000.0]})
            >>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6, cost_bps=5.0)
            >>> net = pf.as_data(pf.cost_adjusted_returns())
            >>> net.returns.columns
            ['returns']
        """
        return Portfolio._build_data_bridge(self.returns if returns is None else returns)

    @property
    @cached_in_slot("_stats_cache")
    def stats(self) -> "Stats":
        """Return a Stats object built from the portfolio's daily returns.

        Delegates to the legacy `Stats` pipeline via
        `data`, so all analytics (Sharpe, drawdown, summary, etc.) are
        available through the shared implementation.

        The result is cached after first access so repeated calls are O(1).
        """
        return self.data.stats

    @property
    @cached_in_slot("_plots_cache")
    def plots(self) -> PortfolioPlots:
        """Convenience accessor returning a PortfolioPlots facade for this portfolio.

        Use this to create Plotly visualizations such as snapshots, lagged
        performance curves, and lead/lag IR charts.

        Returns:
            `PortfolioPlots`: Helper object with
            plotting methods.

        The result is cached after first access so repeated calls are O(1).
        """
        return PortfolioPlots(self)

    @property
    @cached_in_slot("_report_cache")
    def report(self) -> Report:
        """Convenience accessor returning a Report facade for this portfolio.

        Use this to generate a self-contained HTML performance report
        containing statistics tables and interactive charts.

        Returns:
            `Report`: Helper object with
            report methods.

        The result is cached after first access so repeated calls are O(1).
        """
        return Report(self)

    @property
    @cached_in_slot("_utils_cache")
    def utils(self) -> "PortfolioUtils":
        """Convenience accessor returning a PortfolioUtils facade for this portfolio.

        Use this for common data transformations such as converting returns to
        prices, computing log returns, rebasing, aggregating by period, and
        computing exponential standard deviation.

        Returns:
            `PortfolioUtils`: Helper object with
            utility transform methods.

        The result is cached after first access so repeated calls are O(1).
        """
        return PortfolioUtils(self)

assets property

List the asset column names from prices (numeric columns).

Returns:

Type Description
list[str]

list[str]: Names of numeric columns in prices; typically excludes

list[str]

'date'.

cost_model property

Return the active cost model as a CostModel instance.

Returns:

Type Description
CostModel

A CostModel whose cost_per_unit and cost_bps fields

CostModel

reflect the values stored on this portfolio.

data property

Build a legacy Data object from this portfolio's returns.

This bridges the two entry points: Portfolio compiles the NAV curve from prices and positions; the returned Data object gives access to the full legacy analytics pipeline (data.stats, data.plots, data.reports).

Returns:

Type Description
Data

Data: A Data object whose returns column

Data

is the portfolio's daily return series and whose index holds the date

Data

column (or a synthetic integer index for date-free portfolios).

Examples:

>>> import polars as pl
>>> from datetime import date
>>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
>>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
>>> d = pf.data
>>> "returns" in d.returns.columns
True

plots property

Convenience accessor returning a PortfolioPlots facade for this portfolio.

Use this to create Plotly visualizations such as snapshots, lagged performance curves, and lead/lag IR charts.

Returns:

Type Description
PortfolioPlots

PortfolioPlots: Helper object with

PortfolioPlots

plotting methods.

The result is cached after first access so repeated calls are O(1).

report property

Convenience accessor returning a Report facade for this portfolio.

Use this to generate a self-contained HTML performance report containing statistics tables and interactive charts.

Returns:

Type Description
Report

Report: Helper object with

Report

report methods.

The result is cached after first access so repeated calls are O(1).

stats property

Return a Stats object built from the portfolio's daily returns.

Delegates to the legacy Stats pipeline via data, so all analytics (Sharpe, drawdown, summary, etc.) are available through the shared implementation.

The result is cached after first access so repeated calls are O(1).

utils property

Convenience accessor returning a PortfolioUtils facade for this portfolio.

Use this for common data transformations such as converting returns to prices, computing log returns, rebasing, aggregating by period, and computing exponential standard deviation.

Returns:

Type Description
PortfolioUtils

PortfolioUtils: Helper object with

PortfolioUtils

utility transform methods.

The result is cached after first access so repeated calls are O(1).

__post_init__()

Validate input types, shapes, and parameters, and normalise the date column.

Source code in src/jquantstats/portfolio.py
def __post_init__(self) -> None:
    """Validate input types, shapes, and parameters, and normalise the date column."""
    if not isinstance(self.prices, pl.DataFrame):
        raise InvalidPricesTypeError(type(self.prices).__name__)
    if not isinstance(self.cashposition, pl.DataFrame):
        raise InvalidCashPositionTypeError(type(self.cashposition).__name__)
    # Canonicalise the date axis before anything downstream looks for it; the
    # mixins match ``'date'`` by name, so this must happen on every
    # construction path, including a direct ``Portfolio(...)`` call.
    object.__setattr__(self, "prices", _normalise_date_column(self.prices))
    object.__setattr__(self, "cashposition", _normalise_date_column(self.cashposition))
    if self.cashposition.shape[0] != self.prices.shape[0]:
        raise RowCountMismatchError(self.prices.shape[0], self.cashposition.shape[0])
    if self.aum <= 0.0:
        raise NonPositiveAumError(self.aum)
    for slot in _CACHE_SLOTS:
        object.__setattr__(self, slot, None)

__repr__()

Return a string representation of the Portfolio object.

Source code in src/jquantstats/portfolio.py
def __repr__(self) -> str:
    """Return a string representation of the Portfolio object."""
    rows, start, end = self._date_range()
    if start is not None:
        return f"Portfolio(assets={self.assets}, rows={rows}, start={start}, end={end})"
    return f"Portfolio(assets={self.assets}, rows={rows})"

as_data(returns=None)

Bridge a returns-shaped frame into a Data object.

data is the same bridge hardwired to returns. Use this method when the series you want analysed is a derived one — the output of cost_adjusted_returns, deduct_management_fee, or any frame you built yourself — so it reaches Stats through the same narrowing the data property uses.

Building the Data by hand is the trap this exists to close: the portfolio's returns-shaped frames carry 'profit' and 'NAV_accumulated' alongside 'returns', and Data.from_returns(pf.returns) therefore reports a Sharpe ratio for the cash P&L series and the NAV level as if they were two more assets. This method keeps only 'returns'.

Parameters:

Name Type Description Default
returns DataFrame | None

Frame with a 'returns' column, optionally a date column, and any number of other columns (which are ignored). Defaults to returns.

None

Returns:

Type Description
Data

Data: A Data object over the single return series, indexed by the

Data

frame's date column when it has one and by row position otherwise.

Raises:

Type Description
MissingReturnsColumnError

If returns has no 'returns' column.

Examples:

>>> import polars as pl
>>> from datetime import date
>>> _d = [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)]
>>> prices = pl.DataFrame({"date": _d, "A": [100.0, 110.0, 121.0]})
>>> pos = pl.DataFrame({"date": _d, "A": [1000.0, 1000.0, 1000.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6, cost_bps=5.0)
>>> net = pf.as_data(pf.cost_adjusted_returns())
>>> net.returns.columns
['returns']
Source code in src/jquantstats/portfolio.py
def as_data(self, returns: pl.DataFrame | None = None) -> "Data":
    """Bridge a returns-shaped frame into a `Data` object.

    `data` is the same bridge hardwired to `returns`.  Use this
    method when the series you want analysed is a *derived* one — the output
    of `cost_adjusted_returns`, `deduct_management_fee`, or any frame you
    built yourself — so it reaches `Stats` through the same narrowing the
    `data` property uses.

    Building the `Data` by hand is the trap this exists to close:
    the portfolio's returns-shaped frames carry ``'profit'`` and
    ``'NAV_accumulated'`` alongside ``'returns'``, and
    ``Data.from_returns(pf.returns)`` therefore reports a Sharpe ratio for
    the cash P&L series and the NAV level as if they were two more assets.
    This method keeps only ``'returns'``.

    Args:
        returns: Frame with a ``'returns'`` column, optionally a date
            column, and any number of other columns (which are ignored).
            Defaults to `returns`.

    Returns:
        `Data`: A Data object over the single return series, indexed by the
        frame's date column when it has one and by row position otherwise.

    Raises:
        MissingReturnsColumnError: If *returns* has no ``'returns'`` column.

    Examples:
        >>> import polars as pl
        >>> from datetime import date
        >>> _d = [date(2020, 1, 1), date(2020, 1, 2), date(2020, 1, 3)]
        >>> prices = pl.DataFrame({"date": _d, "A": [100.0, 110.0, 121.0]})
        >>> pos = pl.DataFrame({"date": _d, "A": [1000.0, 1000.0, 1000.0]})
        >>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6, cost_bps=5.0)
        >>> net = pf.as_data(pf.cost_adjusted_returns())
        >>> net.returns.columns
        ['returns']
    """
    return Portfolio._build_data_bridge(self.returns if returns is None else returns)

describe()

Return a tidy summary of shape, date range and asset names.

Returns:

pl.DataFrame One row per asset with columns: asset, start, end, rows.

Examples:

>>> import polars as pl
>>> from datetime import date
>>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
>>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
>>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
>>> df = pf.describe()
>>> list(df.columns)
['asset', 'start', 'end', 'rows']
Source code in src/jquantstats/portfolio.py
def describe(self) -> pl.DataFrame:
    """Return a tidy summary of shape, date range and asset names.

    Returns:
    -------
    pl.DataFrame
        One row per asset with columns: asset, start, end, rows.

    Examples:
        >>> import polars as pl
        >>> from datetime import date
        >>> prices = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [100.0, 110.0]})
        >>> pos = pl.DataFrame({"date": [date(2020, 1, 1), date(2020, 1, 2)], "A": [1000.0, 1000.0]})
        >>> pf = Portfolio(prices=prices, cashposition=pos, aum=1e6)
        >>> df = pf.describe()
        >>> list(df.columns)
        ['asset', 'start', 'end', 'rows']
    """
    rows, start, end = self._date_range()
    return pl.DataFrame(
        {
            "asset": self.assets,
            "start": [start] * len(self.assets),
            "end": [end] * len(self.assets),
            "rows": [rows] * len(self.assets),
        }
    )

from_cash_position(prices, cash_position, aum, cost_per_unit=0.0, cost_bps=0.0, cost_model=None, annual_fee=0.0) classmethod

Create a Portfolio directly from cash positions aligned with prices.

Parameters:

Name Type Description Default
prices DataFrame

Price levels per asset over time. A temporal column is normalised to 'date' at construction, whatever it was named.

required
cash_position DataFrame | Expr

Cash exposure per asset over time, either as a DataFrame or as a Polars expression evaluated against prices.

required
aum float

Assets under management used as the base NAV offset.

required
cost_per_unit float

One-way trading cost per unit of position change. Defaults to 0.0 (no cost). Ignored when cost_model is given.

0.0
cost_bps float

One-way trading cost in basis points of AUM turnover. Defaults to 0.0 (no cost). Ignored when cost_model is given.

0.0
cost_model CostModel | None

Optional CostModel instance. When supplied, its cost_per_unit and cost_bps values take precedence over the individual parameters above.

None
annual_fee float

Flat annual management fee as a fraction of AUM (e.g. 0.0085 for 85 bps p.a.). Defaults to 0.0 (no fee). Used as the default by deduct_management_fee.

0.0

Returns:

Type Description
Self

A Portfolio instance with the provided cash positions.

Raises:

Type Description
PositionExprColumnError

If cash_position is an expression that creates columns not present in prices (e.g. via .alias); such expressions leave the original asset columns untouched, silently treating raw prices as positions.

Source code in src/jquantstats/portfolio.py
@classmethod
def from_cash_position(
    cls,
    prices: pl.DataFrame,
    cash_position: pl.DataFrame | pl.Expr,
    aum: float,
    cost_per_unit: float = 0.0,
    cost_bps: float = 0.0,
    cost_model: CostModel | None = None,
    annual_fee: float = 0.0,
) -> Self:
    """Create a Portfolio directly from cash positions aligned with prices.

    Args:
        prices: Price levels per asset over time.  A temporal column is
            normalised to ``'date'`` at construction, whatever it was named.
        cash_position: Cash exposure per asset over time, either as a
            DataFrame or as a Polars expression evaluated against *prices*.
        aum: Assets under management used as the base NAV offset.
        cost_per_unit: One-way trading cost per unit of position change.
            Defaults to 0.0 (no cost).  Ignored when *cost_model* is given.
        cost_bps: One-way trading cost in basis points of AUM turnover.
            Defaults to 0.0 (no cost).  Ignored when *cost_model* is given.
        cost_model: Optional `CostModel`
            instance.  When supplied, its ``cost_per_unit`` and
            ``cost_bps`` values take precedence over the individual
            parameters above.
        annual_fee: Flat annual management fee as a fraction of AUM
            (e.g. 0.0085 for 85 bps p.a.).  Defaults to 0.0 (no fee).
            Used as the default by `deduct_management_fee`.

    Returns:
        A Portfolio instance with the provided cash positions.

    Raises:
        PositionExprColumnError: If *cash_position* is an expression that
            creates columns not present in *prices* (e.g. via ``.alias``);
            such expressions leave the original asset columns untouched,
            silently treating raw prices as positions.
    """
    if isinstance(cash_position, pl.Expr):
        cash_position = _evaluate_position_expr(prices, cash_position, "cash_position")
    if cost_model is not None:
        cost_per_unit = cost_model.cost_per_unit
        cost_bps = cost_model.cost_bps
    return cls(
        prices=prices,
        cashposition=cash_position,
        aum=aum,
        cost_per_unit=cost_per_unit,
        cost_bps=cost_bps,
        annual_fee=annual_fee,
    )

Data

jquantstats.Data dataclass

Bases: _ReshapeMixin

A container for financial returns data and an optional benchmark.

Provides methods for analyzing and manipulating financial returns data, including resampling, truncation, and access to statistical metrics and visualizations via the stats and plots properties.

Attributes:

Name Type Description
returns DataFrame

DataFrame containing returns data with assets as columns.

benchmark DataFrame | None

Optional benchmark returns DataFrame. Defaults to None.

index DataFrame

DataFrame containing the date index for the returns data. A temporal index column is always named 'date' — see Date column below.

Date column

Whatever the input frames called it, a temporal index column ends up as 'date', the same name the Portfolio internals use. So data.index['date'] works on every Data object, however it was built, and Data.date_col stays the safest way to read the name — a date-free (integer-indexed) object keeps its own index column name.

Source code in src/jquantstats/data.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
@dataclasses.dataclass(frozen=True, slots=True)
class Data(_ReshapeMixin):
    """A container for financial returns data and an optional benchmark.

    Provides methods for analyzing and manipulating financial returns data,
    including resampling, truncation, and access to statistical metrics and
    visualizations via the ``stats`` and ``plots`` properties.

    Attributes:
        returns (pl.DataFrame): DataFrame containing returns data with assets
            as columns.
        benchmark (pl.DataFrame | None): Optional benchmark returns DataFrame.
            Defaults to None.
        index (pl.DataFrame): DataFrame containing the date index for the
            returns data. A temporal index column is always named ``'date'``
            — see *Date column* below.

    Date column:
        Whatever the input frames called it, a temporal index column ends up
        as ``'date'``, the same name the `Portfolio` internals use. So
        ``data.index['date']`` works on every `Data` object, however it was
        built, and ``Data.date_col`` stays the safest way to read the name —
        a date-free (integer-indexed) object keeps its own index column name.

    """

    returns: pl.DataFrame
    index: pl.DataFrame
    benchmark: pl.DataFrame | None = None

    def __post_init__(self) -> None:
        """Normalise the index column name and validate the Data object."""
        # Canonicalise here rather than in the constructors alone, so a Data built
        # by hand (or rebuilt by a reshape) carries the same index column name.
        temporal = [name for name, dtype in self.index.schema.items() if dtype.is_temporal()]
        if temporal and temporal[0] != DATE_COLUMN:
            object.__setattr__(self, "index", self.index.rename({temporal[0]: DATE_COLUMN}))

        # You need at least two points
        if self.index.shape[0] < 2:
            raise ValueError("Index must contain at least two timestamps.")  # noqa: TRY003

        # Check index is monotonically increasing
        datetime_col = self.index[self.index.columns[0]]
        if not datetime_col.is_sorted():
            raise ValueError("Index must be monotonically increasing.")  # noqa: TRY003

        # Check row count matches returns
        if self.returns.shape[0] != self.index.shape[0]:
            raise ValueError("Returns and index must have the same number of rows.")  # noqa: TRY003

        # Check row count matches benchmark (if provided)
        if self.benchmark is not None and self.benchmark.shape[0] != self.index.shape[0]:
            raise ValueError("Benchmark and index must have the same number of rows.")  # noqa: TRY003

    @classmethod
    def from_returns(
        cls,
        returns: NativeFrame,
        rf: NativeFrameOrScalar = 0.0,
        benchmark: NativeFrame | None = None,
        date_col: str | None = None,
        null_strategy: Literal["raise", "drop", "forward_fill"] | None = None,
    ) -> Data:
        """Create a Data object from returns and optional benchmark.

        Args:
            returns (NativeFrame): Financial returns data. First column should
                be the date column, remaining columns are asset returns.
            rf (float | NativeFrame): Risk-free rate. Defaults to 0.0 (no
                risk-free rate adjustment).

                - If float: Constant risk-free rate applied to all dates.
                - If NativeFrame: Time-varying risk-free rate with dates
                  matching returns.

            benchmark (NativeFrame | None): Benchmark returns. Defaults to
                None (no benchmark). First column should be the date column,
                remaining columns are benchmark returns. Returns and
                benchmark are aligned on their common dates; if either frame
                contains dates the other lacks, those rows are dropped and a
                `BenchmarkAlignmentWarning` is emitted.
            date_col (str | None): Name of the date column in the DataFrames.
                Defaults to ``None``, which auto-detects the first temporal
                column of each frame. Pass a name to nominate a column
                explicitly — required for a non-temporal (e.g. integer) index,
                which auto-detection will not find. Whichever column is used,
                it is renamed to ``'date'`` on the resulting object.
            null_strategy ({"raise", "drop", "forward_fill"} | None): How to
                handle ``null`` (missing) values in *returns* and *benchmark*.
                Defaults to ``None`` (nulls propagate through calculations).

                - ``None`` — no null checking; nulls propagate through all
                  downstream calculations.
                - ``"raise"`` — raise `NullsInReturnsError` if any null is
                  found.
                - ``"drop"`` — silently drop every row that contains at least
                  one null.
                - ``"forward_fill"`` — fill each null with the most recent
                  non-null value in the same column.

                Note: Affects only Polars ``null`` values (i.e. ``None`` /
                missing entries). IEEE-754 ``NaN`` values are **not** affected
                and continue to propagate as per IEEE-754 semantics.

        Returns:
            Data: Object containing excess returns and benchmark (if any),
            with methods for analysis and visualization through the ``stats``
            and ``plots`` properties.

        Raises:
            MissingDateColumnError: If *date_col* is not a column of
                *returns*, *benchmark*, or a DataFrame-valued *rf* — or, when
                *date_col* is ``None``, if one of those frames has no temporal
                column to auto-detect. Raised before any joins so the
                offending frame is named explicitly.
            NullsInReturnsError: If *null_strategy* is ``"raise"`` and the
                data contains null values.
            ValueError: If there are no overlapping dates between returns and
                benchmark.

        Warns:
            BenchmarkAlignmentWarning: If aligning returns and benchmark on
                their common dates drops rows from either frame.

        Examples:
            Basic usage. Note the ``Date`` column is canonicalised to ``date``:

            >>> import polars as pl
            >>> from jquantstats import Data
            >>> returns = pl.DataFrame(
            ...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [0.01, -0.02, 0.03]}
            ... ).with_columns(pl.col("Date").str.to_date())
            >>> data = Data.from_returns(returns=returns)
            >>> data.assets
            ['Asset1']
            >>> data.all.columns
            ['date', 'Asset1']

            With benchmark and risk-free rate:

            >>> benchmark = pl.DataFrame(
            ...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Market": [0.005, -0.01, 0.02]}
            ... ).with_columns(pl.col("Date").str.to_date())
            >>> data = Data.from_returns(returns=returns, benchmark=benchmark, rf=0.0002)
            >>> data.benchmark.columns
            ['Market']

            Handling nulls automatically. ``"drop"`` mirrors pandas/'uantStats
            behaviour and loses the offending row; ``"forward_fill"`` keeps it:

            >>> returns_with_nulls = pl.DataFrame(
            ...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [0.01, None, 0.03]}
            ... ).with_columns(pl.col("Date").str.to_date())
            >>> Data.from_returns(returns=returns_with_nulls, null_strategy="drop").returns["Asset1"].to_list()
            [0.01, 0.03]
            >>> Data.from_returns(
            ...     returns=returns_with_nulls, null_strategy="forward_fill"
            ... ).returns["Asset1"].to_list()
            [0.01, 0.01, 0.03]

        """
        # Resolve the date column once, up front: a temporal axis is renamed to the
        # canonical 'date' on every frame, so the joins below — and the resulting
        # object — speak one column name whatever the caller's frames were labelled.
        # A frame-valued rf carries the same date_col, so it resolves the same way.
        returns_pl, resolved_col = _canonicalise_date_column(_to_polars(returns), "returns", date_col)
        benchmark_pl = (
            _canonicalise_date_column(_to_polars(benchmark), "benchmark", date_col)[0]
            if benchmark is not None
            else None
        )
        # accept ints (e.g. rf=0) by coercing to float
        rf_converted: float | pl.DataFrame = (
            float(rf) if isinstance(rf, int | float) else _canonicalise_date_column(_to_polars(rf), "rf", date_col)[0]
        )

        returns_pl = _apply_null_strategy(returns_pl, resolved_col, "returns", null_strategy)
        if benchmark_pl is not None:
            benchmark_pl = _apply_null_strategy(benchmark_pl, resolved_col, "benchmark", null_strategy)
            returns_pl, benchmark_pl = _align_returns_benchmark(returns_pl, benchmark_pl, resolved_col)

        index = returns_pl.select(resolved_col)
        excess_returns = _subtract_risk_free(returns_pl, rf_converted, resolved_col).drop(resolved_col)
        excess_benchmark = (
            _subtract_risk_free(benchmark_pl, rf_converted, resolved_col).drop(resolved_col)
            if benchmark_pl is not None
            else None
        )

        return cls(returns=excess_returns, benchmark=excess_benchmark, index=index)

    @classmethod
    def from_prices(
        cls,
        prices: NativeFrame,
        rf: NativeFrameOrScalar = 0.0,
        benchmark: NativeFrame | None = None,
        date_col: str | None = None,
        null_strategy: Literal["raise", "drop", "forward_fill"] | None = None,
    ) -> Data:
        """Create a Data object from prices and optional benchmark.

        Converts price levels to returns via percentage change and delegates
        to `from_returns`. The first row of each asset is dropped because no
        prior price is available to compute a return.

        Args:
            prices (NativeFrame): Price-level data. First column should be
                the date column; remaining columns are asset prices.
            rf (float | NativeFrame): Risk-free rate. Forwarded to
                `from_returns`; a frame-valued *rf* has its date column
                canonicalised on the way, exactly as *prices* does. Defaults
                to 0.0 (no risk-free rate adjustment).
            benchmark (NativeFrame | None): Benchmark prices. Converted to
                returns in the same way as ``prices`` before being forwarded
                to `from_returns`. Defaults to None (no benchmark).
            date_col (str | None): Name of the date column in the DataFrames.
                Defaults to ``None``, which auto-detects the first temporal
                column of each frame. Forwarded unchanged to `from_returns`;
                whichever column is used is renamed to ``'date'`` on the
                resulting object.
            null_strategy ({"raise", "drop", "forward_fill"} | None): How to
                handle ``null`` (missing) values after converting prices to
                returns. Forwarded unchanged to `from_returns`. Defaults to
                ``None`` (nulls propagate through calculations).

                - ``None`` — no null checking; nulls propagate.
                - ``"raise"`` — raise `NullsInReturnsError` if any null is
                  found in the derived returns.
                - ``"drop"`` — silently drop every row that contains at least
                  one null.
                - ``"forward_fill"`` — fill each null with the most recent
                  non-null value.

                Note: Prices that contain nulls will produce null returns via
                ``pct_change()``. If you expect missing price entries, pass
                ``null_strategy="drop"`` or ``null_strategy="forward_fill"``.

        Returns:
            Data: Object containing excess returns derived from the supplied
            prices, with methods for analysis and visualization through the
            ``stats`` and ``plots`` properties.

        Raises:
            MissingDateColumnError: If *date_col* is not a column of *prices*
                or *benchmark* — or, when *date_col* is ``None``, if either
                frame has no temporal column to auto-detect. Raised before
                returns are derived so the offending frame is named explicitly.

        Examples:
            Prices become returns, so the first row is consumed:

            >>> import polars as pl
            >>> from jquantstats import Data
            >>> prices = pl.DataFrame(
            ...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [100.0, 101.0, 99.0]}
            ... ).with_columns(pl.col("Date").str.to_date())
            >>> data = Data.from_prices(prices=prices)
            >>> data.returns.height
            2
            >>> [round(r, 6) for r in data.returns["Asset1"].to_list()]
            [0.01, -0.019802]

        """
        prices_pl, resolved_col = _canonicalise_date_column(_to_polars(prices), "prices", date_col)
        returns_pl = _prices_to_returns(prices_pl, resolved_col)

        benchmark_returns: NativeFrame | None = None
        if benchmark is not None:
            benchmark_pl, _ = _canonicalise_date_column(_to_polars(benchmark), "benchmark", date_col)
            benchmark_returns = _prices_to_returns(benchmark_pl, resolved_col)

        # A frame-valued rf is canonicalised here too, since the frames handed on
        # below are resolved already and date_col no longer describes them.
        rf_forwarded: NativeFrameOrScalar = (
            rf if isinstance(rf, int | float) else _canonicalise_date_column(_to_polars(rf), "rf", date_col)[0]
        )

        # Naming the resolved column keeps auto-detection from picking a different
        # one downstream — it would not find a non-temporal (e.g. integer) index.
        return cls.from_returns(
            returns=returns_pl,
            rf=rf_forwarded,
            benchmark=benchmark_returns,
            date_col=resolved_col,
            null_strategy=null_strategy,
        )

    def __repr__(self) -> str:
        """Return a string representation of the Data object."""
        rows = len(self.index)
        date_cols = self.date_col
        if date_cols:
            date_column = date_cols[0]
            start = self.index[date_column].min()
            end = self.index[date_column].max()
            return f"Data(assets={self.assets}, rows={rows}, start={start!s}, end={end!s})"
        return f"Data(assets={self.assets}, rows={rows})"  # pragma: no cover  # __post_init__ requires ≥1 index column

    @property
    def plots(self) -> DataPlots:
        """Provides access to visualization methods for the financial data.

        Returns:
            DataPlots: An instance of the DataPlots class initialized with this data.

        """
        return DataPlots(self)

    @property
    def stats(self) -> Stats:
        """Provides access to statistical analysis methods for the financial data.

        Returns:
            Stats: An instance of the Stats class initialized with this data.

        """
        return Stats(self)

    @property
    def reports(self) -> Reports:
        """Provides access to reporting methods for the financial data.

        Returns:
            Reports: An instance of the Reports class initialized with this data.

        """
        return Reports(self)

    @property
    def utils(self) -> DataUtils:
        """Provides access to utility transforms and conversions for the financial data.

        Returns:
            DataUtils: An instance of the DataUtils class initialized with this data.

        """
        return DataUtils(self)

    @property
    def date_col(self) -> list[str]:
        """Return the column names of the index DataFrame.

        Returns:
            list[str]: List of column names in the index DataFrame, typically containing
                      the date column name.

        """
        return list(self.index.columns)

    @property
    def assets(self) -> list[str]:
        """Return the combined list of asset column names from returns and benchmark.

        Returns:
            list[str]: List of all asset column names from both returns and benchmark
                      (if available).

        """
        if self.benchmark is not None:
            return list(self.returns.columns) + list(self.benchmark.columns)
        return list(self.returns.columns)

    @property
    def all(self) -> pl.DataFrame:
        """Combine index, returns, and benchmark data into a single DataFrame.

        This property provides a convenient way to access all data in a single DataFrame,
        which is useful for analysis and visualization.

        Returns:
            pl.DataFrame: A DataFrame containing the index, all returns data, and benchmark data
                         (if available) combined horizontally.

        """
        if self.benchmark is None:
            return pl.concat([self.index, self.returns], how="horizontal_extend")
        else:
            return pl.concat([self.index, self.returns, self.benchmark], how="horizontal_extend")

    def describe(self) -> pl.DataFrame:
        """Return a tidy summary of shape, date range and asset names.

        Returns:
            pl.DataFrame: One row per asset with columns: asset, start, end,
            rows, has_benchmark.

        """
        date_column = self.date_col[0]
        start = self.index[date_column].min()
        end = self.index[date_column].max()
        rows = len(self.index)
        return pl.DataFrame(
            {
                "asset": self.returns.columns,
                "start": [start] * len(self.returns.columns),
                "end": [end] * len(self.returns.columns),
                "rows": [rows] * len(self.returns.columns),
                "has_benchmark": [self.benchmark is not None] * len(self.returns.columns),
            }
        )

    @property
    def _periods_per_year(self) -> float:
        """Estimate the number of periods per year based on average frequency in the index.

        For temporal (Date/Datetime) indices, computes the mean gap between observations
        and converts to an annualised period count (e.g. ~252 for daily, ~52 for weekly).

        For integer indices (date-free portfolios), falls back to 252 trading days per year
        because integer diffs have no time meaning.
        """
        datetime_col = self.index[self.index.columns[0]]

        if not datetime_col.dtype.is_temporal():
            return 252.0

        sorted_dt = datetime_col.sort()
        diffs = sorted_dt.diff().drop_nulls()
        mean_diff = diffs.mean()

        if isinstance(mean_diff, timedelta):
            seconds = mean_diff.total_seconds()
        else:  # pragma: no cover  # Polars always returns timedelta for temporal diff
            seconds = cast(float, mean_diff) if mean_diff is not None else 1.0

        return (365 * 24 * 60 * 60) / seconds

    def items(self) -> Iterator[tuple[str, pl.Series]]:
        """Iterate over all assets and their corresponding data series.

        This method provides a convenient way to iterate over all assets in the data,
        yielding each asset name and its corresponding data series.

        Yields:
            tuple[str, pl.Series]: A tuple containing the asset name and its data series.

        """
        matrix = self.all

        for col in self.assets:
            yield col, matrix.get_column(col)

all property

Combine index, returns, and benchmark data into a single DataFrame.

This property provides a convenient way to access all data in a single DataFrame, which is useful for analysis and visualization.

Returns:

Type Description
DataFrame

pl.DataFrame: A DataFrame containing the index, all returns data, and benchmark data (if available) combined horizontally.

assets property

Return the combined list of asset column names from returns and benchmark.

Returns:

Type Description
list[str]

list[str]: List of all asset column names from both returns and benchmark (if available).

date_col property

Return the column names of the index DataFrame.

Returns:

Type Description
list[str]

list[str]: List of column names in the index DataFrame, typically containing the date column name.

plots property

Provides access to visualization methods for the financial data.

Returns:

Name Type Description
DataPlots DataPlots

An instance of the DataPlots class initialized with this data.

reports property

Provides access to reporting methods for the financial data.

Returns:

Name Type Description
Reports Reports

An instance of the Reports class initialized with this data.

stats property

Provides access to statistical analysis methods for the financial data.

Returns:

Name Type Description
Stats Stats

An instance of the Stats class initialized with this data.

utils property

Provides access to utility transforms and conversions for the financial data.

Returns:

Name Type Description
DataUtils DataUtils

An instance of the DataUtils class initialized with this data.

__post_init__()

Normalise the index column name and validate the Data object.

Source code in src/jquantstats/data.py
def __post_init__(self) -> None:
    """Normalise the index column name and validate the Data object."""
    # Canonicalise here rather than in the constructors alone, so a Data built
    # by hand (or rebuilt by a reshape) carries the same index column name.
    temporal = [name for name, dtype in self.index.schema.items() if dtype.is_temporal()]
    if temporal and temporal[0] != DATE_COLUMN:
        object.__setattr__(self, "index", self.index.rename({temporal[0]: DATE_COLUMN}))

    # You need at least two points
    if self.index.shape[0] < 2:
        raise ValueError("Index must contain at least two timestamps.")  # noqa: TRY003

    # Check index is monotonically increasing
    datetime_col = self.index[self.index.columns[0]]
    if not datetime_col.is_sorted():
        raise ValueError("Index must be monotonically increasing.")  # noqa: TRY003

    # Check row count matches returns
    if self.returns.shape[0] != self.index.shape[0]:
        raise ValueError("Returns and index must have the same number of rows.")  # noqa: TRY003

    # Check row count matches benchmark (if provided)
    if self.benchmark is not None and self.benchmark.shape[0] != self.index.shape[0]:
        raise ValueError("Benchmark and index must have the same number of rows.")  # noqa: TRY003

__repr__()

Return a string representation of the Data object.

Source code in src/jquantstats/data.py
def __repr__(self) -> str:
    """Return a string representation of the Data object."""
    rows = len(self.index)
    date_cols = self.date_col
    if date_cols:
        date_column = date_cols[0]
        start = self.index[date_column].min()
        end = self.index[date_column].max()
        return f"Data(assets={self.assets}, rows={rows}, start={start!s}, end={end!s})"
    return f"Data(assets={self.assets}, rows={rows})"  # pragma: no cover  # __post_init__ requires ≥1 index column

describe()

Return a tidy summary of shape, date range and asset names.

Returns:

Type Description
DataFrame

pl.DataFrame: One row per asset with columns: asset, start, end,

DataFrame

rows, has_benchmark.

Source code in src/jquantstats/data.py
def describe(self) -> pl.DataFrame:
    """Return a tidy summary of shape, date range and asset names.

    Returns:
        pl.DataFrame: One row per asset with columns: asset, start, end,
        rows, has_benchmark.

    """
    date_column = self.date_col[0]
    start = self.index[date_column].min()
    end = self.index[date_column].max()
    rows = len(self.index)
    return pl.DataFrame(
        {
            "asset": self.returns.columns,
            "start": [start] * len(self.returns.columns),
            "end": [end] * len(self.returns.columns),
            "rows": [rows] * len(self.returns.columns),
            "has_benchmark": [self.benchmark is not None] * len(self.returns.columns),
        }
    )

from_prices(prices, rf=0.0, benchmark=None, date_col=None, null_strategy=None) classmethod

Create a Data object from prices and optional benchmark.

Converts price levels to returns via percentage change and delegates to from_returns. The first row of each asset is dropped because no prior price is available to compute a return.

Parameters:

Name Type Description Default
prices NativeFrame

Price-level data. First column should be the date column; remaining columns are asset prices.

required
rf float | NativeFrame

Risk-free rate. Forwarded to from_returns; a frame-valued rf has its date column canonicalised on the way, exactly as prices does. Defaults to 0.0 (no risk-free rate adjustment).

0.0
benchmark NativeFrame | None

Benchmark prices. Converted to returns in the same way as prices before being forwarded to from_returns. Defaults to None (no benchmark).

None
date_col str | None

Name of the date column in the DataFrames. Defaults to None, which auto-detects the first temporal column of each frame. Forwarded unchanged to from_returns; whichever column is used is renamed to 'date' on the resulting object.

None
null_strategy {'raise', 'drop', 'forward_fill'} | None

How to handle null (missing) values after converting prices to returns. Forwarded unchanged to from_returns. Defaults to None (nulls propagate through calculations).

  • None — no null checking; nulls propagate.
  • "raise" — raise NullsInReturnsError if any null is found in the derived returns.
  • "drop" — silently drop every row that contains at least one null.
  • "forward_fill" — fill each null with the most recent non-null value.

Note: Prices that contain nulls will produce null returns via pct_change(). If you expect missing price entries, pass null_strategy="drop" or null_strategy="forward_fill".

None

Returns:

Name Type Description
Data Data

Object containing excess returns derived from the supplied

Data

prices, with methods for analysis and visualization through the

Data

stats and plots properties.

Raises:

Type Description
MissingDateColumnError

If date_col is not a column of prices or benchmark — or, when date_col is None, if either frame has no temporal column to auto-detect. Raised before returns are derived so the offending frame is named explicitly.

Examples:

Prices become returns, so the first row is consumed:

>>> import polars as pl
>>> from jquantstats import Data
>>> prices = pl.DataFrame(
...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [100.0, 101.0, 99.0]}
... ).with_columns(pl.col("Date").str.to_date())
>>> data = Data.from_prices(prices=prices)
>>> data.returns.height
2
>>> [round(r, 6) for r in data.returns["Asset1"].to_list()]
[0.01, -0.019802]
Source code in src/jquantstats/data.py
@classmethod
def from_prices(
    cls,
    prices: NativeFrame,
    rf: NativeFrameOrScalar = 0.0,
    benchmark: NativeFrame | None = None,
    date_col: str | None = None,
    null_strategy: Literal["raise", "drop", "forward_fill"] | None = None,
) -> Data:
    """Create a Data object from prices and optional benchmark.

    Converts price levels to returns via percentage change and delegates
    to `from_returns`. The first row of each asset is dropped because no
    prior price is available to compute a return.

    Args:
        prices (NativeFrame): Price-level data. First column should be
            the date column; remaining columns are asset prices.
        rf (float | NativeFrame): Risk-free rate. Forwarded to
            `from_returns`; a frame-valued *rf* has its date column
            canonicalised on the way, exactly as *prices* does. Defaults
            to 0.0 (no risk-free rate adjustment).
        benchmark (NativeFrame | None): Benchmark prices. Converted to
            returns in the same way as ``prices`` before being forwarded
            to `from_returns`. Defaults to None (no benchmark).
        date_col (str | None): Name of the date column in the DataFrames.
            Defaults to ``None``, which auto-detects the first temporal
            column of each frame. Forwarded unchanged to `from_returns`;
            whichever column is used is renamed to ``'date'`` on the
            resulting object.
        null_strategy ({"raise", "drop", "forward_fill"} | None): How to
            handle ``null`` (missing) values after converting prices to
            returns. Forwarded unchanged to `from_returns`. Defaults to
            ``None`` (nulls propagate through calculations).

            - ``None`` — no null checking; nulls propagate.
            - ``"raise"`` — raise `NullsInReturnsError` if any null is
              found in the derived returns.
            - ``"drop"`` — silently drop every row that contains at least
              one null.
            - ``"forward_fill"`` — fill each null with the most recent
              non-null value.

            Note: Prices that contain nulls will produce null returns via
            ``pct_change()``. If you expect missing price entries, pass
            ``null_strategy="drop"`` or ``null_strategy="forward_fill"``.

    Returns:
        Data: Object containing excess returns derived from the supplied
        prices, with methods for analysis and visualization through the
        ``stats`` and ``plots`` properties.

    Raises:
        MissingDateColumnError: If *date_col* is not a column of *prices*
            or *benchmark* — or, when *date_col* is ``None``, if either
            frame has no temporal column to auto-detect. Raised before
            returns are derived so the offending frame is named explicitly.

    Examples:
        Prices become returns, so the first row is consumed:

        >>> import polars as pl
        >>> from jquantstats import Data
        >>> prices = pl.DataFrame(
        ...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [100.0, 101.0, 99.0]}
        ... ).with_columns(pl.col("Date").str.to_date())
        >>> data = Data.from_prices(prices=prices)
        >>> data.returns.height
        2
        >>> [round(r, 6) for r in data.returns["Asset1"].to_list()]
        [0.01, -0.019802]

    """
    prices_pl, resolved_col = _canonicalise_date_column(_to_polars(prices), "prices", date_col)
    returns_pl = _prices_to_returns(prices_pl, resolved_col)

    benchmark_returns: NativeFrame | None = None
    if benchmark is not None:
        benchmark_pl, _ = _canonicalise_date_column(_to_polars(benchmark), "benchmark", date_col)
        benchmark_returns = _prices_to_returns(benchmark_pl, resolved_col)

    # A frame-valued rf is canonicalised here too, since the frames handed on
    # below are resolved already and date_col no longer describes them.
    rf_forwarded: NativeFrameOrScalar = (
        rf if isinstance(rf, int | float) else _canonicalise_date_column(_to_polars(rf), "rf", date_col)[0]
    )

    # Naming the resolved column keeps auto-detection from picking a different
    # one downstream — it would not find a non-temporal (e.g. integer) index.
    return cls.from_returns(
        returns=returns_pl,
        rf=rf_forwarded,
        benchmark=benchmark_returns,
        date_col=resolved_col,
        null_strategy=null_strategy,
    )

from_returns(returns, rf=0.0, benchmark=None, date_col=None, null_strategy=None) classmethod

Create a Data object from returns and optional benchmark.

Parameters:

Name Type Description Default
returns NativeFrame

Financial returns data. First column should be the date column, remaining columns are asset returns.

required
rf float | NativeFrame

Risk-free rate. Defaults to 0.0 (no risk-free rate adjustment).

  • If float: Constant risk-free rate applied to all dates.
  • If NativeFrame: Time-varying risk-free rate with dates matching returns.
0.0
benchmark NativeFrame | None

Benchmark returns. Defaults to None (no benchmark). First column should be the date column, remaining columns are benchmark returns. Returns and benchmark are aligned on their common dates; if either frame contains dates the other lacks, those rows are dropped and a BenchmarkAlignmentWarning is emitted.

None
date_col str | None

Name of the date column in the DataFrames. Defaults to None, which auto-detects the first temporal column of each frame. Pass a name to nominate a column explicitly — required for a non-temporal (e.g. integer) index, which auto-detection will not find. Whichever column is used, it is renamed to 'date' on the resulting object.

None
null_strategy {'raise', 'drop', 'forward_fill'} | None

How to handle null (missing) values in returns and benchmark. Defaults to None (nulls propagate through calculations).

  • None — no null checking; nulls propagate through all downstream calculations.
  • "raise" — raise NullsInReturnsError if any null is found.
  • "drop" — silently drop every row that contains at least one null.
  • "forward_fill" — fill each null with the most recent non-null value in the same column.

Note: Affects only Polars null values (i.e. None / missing entries). IEEE-754 NaN values are not affected and continue to propagate as per IEEE-754 semantics.

None

Returns:

Name Type Description
Data Data

Object containing excess returns and benchmark (if any),

Data

with methods for analysis and visualization through the stats

Data

and plots properties.

Raises:

Type Description
MissingDateColumnError

If date_col is not a column of returns, benchmark, or a DataFrame-valued rf — or, when date_col is None, if one of those frames has no temporal column to auto-detect. Raised before any joins so the offending frame is named explicitly.

NullsInReturnsError

If null_strategy is "raise" and the data contains null values.

ValueError

If there are no overlapping dates between returns and benchmark.

Warns:

Type Description
BenchmarkAlignmentWarning

If aligning returns and benchmark on their common dates drops rows from either frame.

Examples:

Basic usage. Note the Date column is canonicalised to date:

>>> import polars as pl
>>> from jquantstats import Data
>>> returns = pl.DataFrame(
...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [0.01, -0.02, 0.03]}
... ).with_columns(pl.col("Date").str.to_date())
>>> data = Data.from_returns(returns=returns)
>>> data.assets
['Asset1']
>>> data.all.columns
['date', 'Asset1']

With benchmark and risk-free rate:

>>> benchmark = pl.DataFrame(
...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Market": [0.005, -0.01, 0.02]}
... ).with_columns(pl.col("Date").str.to_date())
>>> data = Data.from_returns(returns=returns, benchmark=benchmark, rf=0.0002)
>>> data.benchmark.columns
['Market']

Handling nulls automatically. "drop" mirrors pandas/'uantStats behaviour and loses the offending row; "forward_fill" keeps it:

>>> returns_with_nulls = pl.DataFrame(
...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [0.01, None, 0.03]}
... ).with_columns(pl.col("Date").str.to_date())
>>> Data.from_returns(returns=returns_with_nulls, null_strategy="drop").returns["Asset1"].to_list()
[0.01, 0.03]
>>> Data.from_returns(
...     returns=returns_with_nulls, null_strategy="forward_fill"
... ).returns["Asset1"].to_list()
[0.01, 0.01, 0.03]
Source code in src/jquantstats/data.py
@classmethod
def from_returns(
    cls,
    returns: NativeFrame,
    rf: NativeFrameOrScalar = 0.0,
    benchmark: NativeFrame | None = None,
    date_col: str | None = None,
    null_strategy: Literal["raise", "drop", "forward_fill"] | None = None,
) -> Data:
    """Create a Data object from returns and optional benchmark.

    Args:
        returns (NativeFrame): Financial returns data. First column should
            be the date column, remaining columns are asset returns.
        rf (float | NativeFrame): Risk-free rate. Defaults to 0.0 (no
            risk-free rate adjustment).

            - If float: Constant risk-free rate applied to all dates.
            - If NativeFrame: Time-varying risk-free rate with dates
              matching returns.

        benchmark (NativeFrame | None): Benchmark returns. Defaults to
            None (no benchmark). First column should be the date column,
            remaining columns are benchmark returns. Returns and
            benchmark are aligned on their common dates; if either frame
            contains dates the other lacks, those rows are dropped and a
            `BenchmarkAlignmentWarning` is emitted.
        date_col (str | None): Name of the date column in the DataFrames.
            Defaults to ``None``, which auto-detects the first temporal
            column of each frame. Pass a name to nominate a column
            explicitly — required for a non-temporal (e.g. integer) index,
            which auto-detection will not find. Whichever column is used,
            it is renamed to ``'date'`` on the resulting object.
        null_strategy ({"raise", "drop", "forward_fill"} | None): How to
            handle ``null`` (missing) values in *returns* and *benchmark*.
            Defaults to ``None`` (nulls propagate through calculations).

            - ``None`` — no null checking; nulls propagate through all
              downstream calculations.
            - ``"raise"`` — raise `NullsInReturnsError` if any null is
              found.
            - ``"drop"`` — silently drop every row that contains at least
              one null.
            - ``"forward_fill"`` — fill each null with the most recent
              non-null value in the same column.

            Note: Affects only Polars ``null`` values (i.e. ``None`` /
            missing entries). IEEE-754 ``NaN`` values are **not** affected
            and continue to propagate as per IEEE-754 semantics.

    Returns:
        Data: Object containing excess returns and benchmark (if any),
        with methods for analysis and visualization through the ``stats``
        and ``plots`` properties.

    Raises:
        MissingDateColumnError: If *date_col* is not a column of
            *returns*, *benchmark*, or a DataFrame-valued *rf* — or, when
            *date_col* is ``None``, if one of those frames has no temporal
            column to auto-detect. Raised before any joins so the
            offending frame is named explicitly.
        NullsInReturnsError: If *null_strategy* is ``"raise"`` and the
            data contains null values.
        ValueError: If there are no overlapping dates between returns and
            benchmark.

    Warns:
        BenchmarkAlignmentWarning: If aligning returns and benchmark on
            their common dates drops rows from either frame.

    Examples:
        Basic usage. Note the ``Date`` column is canonicalised to ``date``:

        >>> import polars as pl
        >>> from jquantstats import Data
        >>> returns = pl.DataFrame(
        ...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [0.01, -0.02, 0.03]}
        ... ).with_columns(pl.col("Date").str.to_date())
        >>> data = Data.from_returns(returns=returns)
        >>> data.assets
        ['Asset1']
        >>> data.all.columns
        ['date', 'Asset1']

        With benchmark and risk-free rate:

        >>> benchmark = pl.DataFrame(
        ...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Market": [0.005, -0.01, 0.02]}
        ... ).with_columns(pl.col("Date").str.to_date())
        >>> data = Data.from_returns(returns=returns, benchmark=benchmark, rf=0.0002)
        >>> data.benchmark.columns
        ['Market']

        Handling nulls automatically. ``"drop"`` mirrors pandas/'uantStats
        behaviour and loses the offending row; ``"forward_fill"`` keeps it:

        >>> returns_with_nulls = pl.DataFrame(
        ...     {"Date": ["2023-01-01", "2023-01-02", "2023-01-03"], "Asset1": [0.01, None, 0.03]}
        ... ).with_columns(pl.col("Date").str.to_date())
        >>> Data.from_returns(returns=returns_with_nulls, null_strategy="drop").returns["Asset1"].to_list()
        [0.01, 0.03]
        >>> Data.from_returns(
        ...     returns=returns_with_nulls, null_strategy="forward_fill"
        ... ).returns["Asset1"].to_list()
        [0.01, 0.01, 0.03]

    """
    # Resolve the date column once, up front: a temporal axis is renamed to the
    # canonical 'date' on every frame, so the joins below — and the resulting
    # object — speak one column name whatever the caller's frames were labelled.
    # A frame-valued rf carries the same date_col, so it resolves the same way.
    returns_pl, resolved_col = _canonicalise_date_column(_to_polars(returns), "returns", date_col)
    benchmark_pl = (
        _canonicalise_date_column(_to_polars(benchmark), "benchmark", date_col)[0]
        if benchmark is not None
        else None
    )
    # accept ints (e.g. rf=0) by coercing to float
    rf_converted: float | pl.DataFrame = (
        float(rf) if isinstance(rf, int | float) else _canonicalise_date_column(_to_polars(rf), "rf", date_col)[0]
    )

    returns_pl = _apply_null_strategy(returns_pl, resolved_col, "returns", null_strategy)
    if benchmark_pl is not None:
        benchmark_pl = _apply_null_strategy(benchmark_pl, resolved_col, "benchmark", null_strategy)
        returns_pl, benchmark_pl = _align_returns_benchmark(returns_pl, benchmark_pl, resolved_col)

    index = returns_pl.select(resolved_col)
    excess_returns = _subtract_risk_free(returns_pl, rf_converted, resolved_col).drop(resolved_col)
    excess_benchmark = (
        _subtract_risk_free(benchmark_pl, rf_converted, resolved_col).drop(resolved_col)
        if benchmark_pl is not None
        else None
    )

    return cls(returns=excess_returns, benchmark=excess_benchmark, index=index)

items()

Iterate over all assets and their corresponding data series.

This method provides a convenient way to iterate over all assets in the data, yielding each asset name and its corresponding data series.

Yields:

Type Description
tuple[str, Series]

tuple[str, pl.Series]: A tuple containing the asset name and its data series.

Source code in src/jquantstats/data.py
def items(self) -> Iterator[tuple[str, pl.Series]]:
    """Iterate over all assets and their corresponding data series.

    This method provides a convenient way to iterate over all assets in the data,
    yielding each asset name and its corresponding data series.

    Yields:
        tuple[str, pl.Series]: A tuple containing the asset name and its data series.

    """
    matrix = self.all

    for col in self.assets:
        yield col, matrix.get_column(col)

CostModel

jquantstats.CostModel dataclass

Unified representation of a portfolio transaction-cost model.

Eliminates the implicit "pick one" contract between the two independent cost parameters (cost_per_unit and cost_bps) on Portfolio. A CostModel instance encapsulates one model at a time and can be passed to any Portfolio factory method instead of specifying the raw float parameters.

Attributes:

Name Type Description
cost_per_unit float

One-way cost per unit of position change (Model A). Defaults to 0.0.

cost_bps float

One-way cost in basis points of AUM turnover (Model B). Defaults to 0.0.

Raises:

Type Description
ValueError

If cost_per_unit or cost_bps is negative, or if both are non-zero (which would silently double-count costs).

Examples:

>>> CostModel.per_unit(0.01)
CostModel(cost_per_unit=0.01, cost_bps=0.0)
>>> CostModel.turnover_bps(5.0)
CostModel(cost_per_unit=0.0, cost_bps=5.0)
>>> CostModel.zero()
CostModel(cost_per_unit=0.0, cost_bps=0.0)
Source code in src/jquantstats/_cost_model.py
@dataclasses.dataclass(frozen=True)
class CostModel:
    """Unified representation of a portfolio transaction-cost model.

    Eliminates the implicit "pick one" contract between the two independent
    cost parameters (``cost_per_unit`` and ``cost_bps``) on
    `Portfolio`.  A ``CostModel``
    instance encapsulates one model at a time and can be passed to any
    Portfolio factory method instead of specifying the raw float parameters.

    Attributes:
        cost_per_unit: One-way cost per unit of position change (Model A).
            Defaults to 0.0.
        cost_bps: One-way cost in basis points of AUM turnover (Model B).
            Defaults to 0.0.

    Raises:
        ValueError: If ``cost_per_unit`` or ``cost_bps`` is negative, or if
            both are non-zero (which would silently double-count costs).

    Examples:
        >>> CostModel.per_unit(0.01)
        CostModel(cost_per_unit=0.01, cost_bps=0.0)
        >>> CostModel.turnover_bps(5.0)
        CostModel(cost_per_unit=0.0, cost_bps=5.0)
        >>> CostModel.zero()
        CostModel(cost_per_unit=0.0, cost_bps=0.0)
    """

    cost_per_unit: float = 0.0
    cost_bps: float = 0.0

    def __post_init__(self) -> None:
        if self.cost_per_unit < 0:
            raise ValueError(f"cost_per_unit must be non-negative, got {self.cost_per_unit}")  # noqa: TRY003
        if self.cost_bps < 0:
            raise ValueError(f"cost_bps must be non-negative, got {self.cost_bps}")  # noqa: TRY003
        if self.cost_per_unit > 0 and self.cost_bps > 0:
            raise ValueError(  # noqa: TRY003
                "Only one cost model may be active at a time: "
                f"got cost_per_unit={self.cost_per_unit} and cost_bps={self.cost_bps}. "
                "Use CostModel.per_unit() or CostModel.turnover_bps() to make intent explicit."
            )

    # ── Named constructors ────────────────────────────────────────────────────

    @classmethod
    def per_unit(cls, cost: float) -> CostModel:
        """Create a Model A (position-delta) cost model.

        Args:
            cost: One-way cost per unit of position change.  Must be
                non-negative.

        Returns:
            A `CostModel` with ``cost_per_unit=cost`` and
            ``cost_bps=0.0``.

        Examples:
            >>> CostModel.per_unit(0.01)
            CostModel(cost_per_unit=0.01, cost_bps=0.0)
        """
        return cls(cost_per_unit=cost, cost_bps=0.0)

    @classmethod
    def turnover_bps(cls, bps: float) -> CostModel:
        """Create a Model B (turnover-bps) cost model.

        Args:
            bps: One-way cost in basis points of AUM turnover.  Must be
                non-negative.

        Returns:
            A `CostModel` with ``cost_per_unit=0.0`` and
            ``cost_bps=bps``.

        Examples:
            >>> CostModel.turnover_bps(5.0)
            CostModel(cost_per_unit=0.0, cost_bps=5.0)
        """
        return cls(cost_per_unit=0.0, cost_bps=bps)

    @classmethod
    def zero(cls) -> CostModel:
        """Create a zero-cost model (no transaction costs).

        Returns:
            A `CostModel` with both parameters set to 0.0.

        Examples:
            >>> CostModel.zero()
            CostModel(cost_per_unit=0.0, cost_bps=0.0)
        """
        return cls(cost_per_unit=0.0, cost_bps=0.0)

per_unit(cost) classmethod

Create a Model A (position-delta) cost model.

Parameters:

Name Type Description Default
cost float

One-way cost per unit of position change. Must be non-negative.

required

Returns:

Type Description
CostModel

A CostModel with cost_per_unit=cost and

CostModel

cost_bps=0.0.

Examples:

>>> CostModel.per_unit(0.01)
CostModel(cost_per_unit=0.01, cost_bps=0.0)
Source code in src/jquantstats/_cost_model.py
@classmethod
def per_unit(cls, cost: float) -> CostModel:
    """Create a Model A (position-delta) cost model.

    Args:
        cost: One-way cost per unit of position change.  Must be
            non-negative.

    Returns:
        A `CostModel` with ``cost_per_unit=cost`` and
        ``cost_bps=0.0``.

    Examples:
        >>> CostModel.per_unit(0.01)
        CostModel(cost_per_unit=0.01, cost_bps=0.0)
    """
    return cls(cost_per_unit=cost, cost_bps=0.0)

turnover_bps(bps) classmethod

Create a Model B (turnover-bps) cost model.

Parameters:

Name Type Description Default
bps float

One-way cost in basis points of AUM turnover. Must be non-negative.

required

Returns:

Type Description
CostModel

A CostModel with cost_per_unit=0.0 and

CostModel

cost_bps=bps.

Examples:

>>> CostModel.turnover_bps(5.0)
CostModel(cost_per_unit=0.0, cost_bps=5.0)
Source code in src/jquantstats/_cost_model.py
@classmethod
def turnover_bps(cls, bps: float) -> CostModel:
    """Create a Model B (turnover-bps) cost model.

    Args:
        bps: One-way cost in basis points of AUM turnover.  Must be
            non-negative.

    Returns:
        A `CostModel` with ``cost_per_unit=0.0`` and
        ``cost_bps=bps``.

    Examples:
        >>> CostModel.turnover_bps(5.0)
        CostModel(cost_per_unit=0.0, cost_bps=5.0)
    """
    return cls(cost_per_unit=0.0, cost_bps=bps)

zero() classmethod

Create a zero-cost model (no transaction costs).

Returns:

Type Description
CostModel

A CostModel with both parameters set to 0.0.

Examples:

>>> CostModel.zero()
CostModel(cost_per_unit=0.0, cost_bps=0.0)
Source code in src/jquantstats/_cost_model.py
@classmethod
def zero(cls) -> CostModel:
    """Create a zero-cost model (no transaction costs).

    Returns:
        A `CostModel` with both parameters set to 0.0.

    Examples:
        >>> CostModel.zero()
        CostModel(cost_per_unit=0.0, cost_bps=0.0)
    """
    return cls(cost_per_unit=0.0, cost_bps=0.0)

Result

Result bundles a Portfolio with an optional per-asset expected-returns frame (mu) and exports a standard artifact set in one call: create_reports(output_dir) writes CSVs (prices, profit, returns, positions, tilt/timing decomposition, and the mu signal when present) plus interactive HTML plots. Use it when you want a reproducible on-disk report bundle from a backtest or experiment; call portfolio.plots / portfolio.report directly when you just want figures in a notebook. mu (when given) must be a Polars DataFrame with one column per portfolio asset — anything else raises at construction time.

jquantstats.Result dataclass

Lightweight container for system outputs.

Attributes:

Name Type Description
portfolio Portfolio

The portfolio constructed by a system/experiment.

mu DataFrame | None

Optional per-asset expected-returns surface used by some systems.

Source code in src/jquantstats/result.py
@dataclass(frozen=True)
class Result:
    """Lightweight container for system outputs.

    Attributes:
        portfolio: The portfolio constructed by a system/experiment.
        mu: Optional per-asset expected-returns surface used by some systems.
    """

    portfolio: Portfolio
    mu: pl.DataFrame | None = None

    def __post_init__(self) -> None:
        """Validate that mu (when given) is a DataFrame covering every portfolio asset.

        Raises:
            TypeError: If ``mu`` is neither ``None`` nor a `polars.DataFrame`.
            MuSchemaError: If ``mu`` lacks a column for one or more portfolio assets.
        """
        if self.mu is None:
            return
        if not isinstance(self.mu, pl.DataFrame):
            raise TypeError(f"mu must be a polars DataFrame or None, got {type(self.mu).__name__}")  # noqa: TRY003
        missing = [asset for asset in self.portfolio.assets if asset not in self.mu.columns]
        if missing:
            raise MuSchemaError(missing)

    def create_reports(self, output_dir: Path) -> None:
        """Generate CSV exports and interactive HTML plots for this result.

        Args:
            output_dir: Destination directory where two subfolders will be created:
                - data/: CSV exports of prices, profit, returns, positions, and signal (if mu present).
                - plots/: Plotly HTML reports (snapshot, lead/lag IR, lagged performance,
                  smoothed holdings performance).
        """
        data = output_dir / "data"
        plots = output_dir / "plots"

        data.mkdir(parents=True, exist_ok=True)
        plots.mkdir(parents=True, exist_ok=True)

        self.portfolio.prices.write_csv(file=data / "prices.csv")
        self.portfolio.profit.write_csv(file=data / "profit.csv")
        self.portfolio.returns.write_csv(file=data / "returns.csv")
        self.portfolio.tilt_timing_decomp.write_csv(file=data / "tilt_timing_decomp.csv")

        if self.mu is not None:
            self.mu.write_csv(file=data / "signal.csv")

        self.portfolio.cashposition.write_csv(file=data / "position.csv")

        # `write_html` is a Plotly-only method, so these charts must be Plotly
        # figures whatever `set_plot_backend` has been told; a matplotlib figure
        # would raise AttributeError here.
        with plot_backend("plotly"):
            fig = self.portfolio.plots.snapshot()
            fig.write_html(file=plots / "snapshot.html", auto_open=False, include_plotlyjs="cdn")
            fig = self.portfolio.plots.lead_lag_ir_plot()
            fig.write_html(file=plots / "lag_ir.html", auto_open=False, include_plotlyjs="cdn")
            fig = self.portfolio.plots.lagged_performance_plot()
            fig.write_html(file=plots / "lagged_perf.html", auto_open=False, include_plotlyjs="cdn")
            fig = self.portfolio.plots.smoothed_holdings_performance_plot()
            fig.write_html(file=plots / "smooth_perf.html", auto_open=False, include_plotlyjs="cdn")

__post_init__()

Validate that mu (when given) is a DataFrame covering every portfolio asset.

Raises:

Type Description
TypeError

If mu is neither None nor a polars.DataFrame.

MuSchemaError

If mu lacks a column for one or more portfolio assets.

Source code in src/jquantstats/result.py
def __post_init__(self) -> None:
    """Validate that mu (when given) is a DataFrame covering every portfolio asset.

    Raises:
        TypeError: If ``mu`` is neither ``None`` nor a `polars.DataFrame`.
        MuSchemaError: If ``mu`` lacks a column for one or more portfolio assets.
    """
    if self.mu is None:
        return
    if not isinstance(self.mu, pl.DataFrame):
        raise TypeError(f"mu must be a polars DataFrame or None, got {type(self.mu).__name__}")  # noqa: TRY003
    missing = [asset for asset in self.portfolio.assets if asset not in self.mu.columns]
    if missing:
        raise MuSchemaError(missing)

create_reports(output_dir)

Generate CSV exports and interactive HTML plots for this result.

Parameters:

Name Type Description Default
output_dir Path

Destination directory where two subfolders will be created: - data/: CSV exports of prices, profit, returns, positions, and signal (if mu present). - plots/: Plotly HTML reports (snapshot, lead/lag IR, lagged performance, smoothed holdings performance).

required
Source code in src/jquantstats/result.py
def create_reports(self, output_dir: Path) -> None:
    """Generate CSV exports and interactive HTML plots for this result.

    Args:
        output_dir: Destination directory where two subfolders will be created:
            - data/: CSV exports of prices, profit, returns, positions, and signal (if mu present).
            - plots/: Plotly HTML reports (snapshot, lead/lag IR, lagged performance,
              smoothed holdings performance).
    """
    data = output_dir / "data"
    plots = output_dir / "plots"

    data.mkdir(parents=True, exist_ok=True)
    plots.mkdir(parents=True, exist_ok=True)

    self.portfolio.prices.write_csv(file=data / "prices.csv")
    self.portfolio.profit.write_csv(file=data / "profit.csv")
    self.portfolio.returns.write_csv(file=data / "returns.csv")
    self.portfolio.tilt_timing_decomp.write_csv(file=data / "tilt_timing_decomp.csv")

    if self.mu is not None:
        self.mu.write_csv(file=data / "signal.csv")

    self.portfolio.cashposition.write_csv(file=data / "position.csv")

    # `write_html` is a Plotly-only method, so these charts must be Plotly
    # figures whatever `set_plot_backend` has been told; a matplotlib figure
    # would raise AttributeError here.
    with plot_backend("plotly"):
        fig = self.portfolio.plots.snapshot()
        fig.write_html(file=plots / "snapshot.html", auto_open=False, include_plotlyjs="cdn")
        fig = self.portfolio.plots.lead_lag_ir_plot()
        fig.write_html(file=plots / "lag_ir.html", auto_open=False, include_plotlyjs="cdn")
        fig = self.portfolio.plots.lagged_performance_plot()
        fig.write_html(file=plots / "lagged_perf.html", auto_open=False, include_plotlyjs="cdn")
        fig = self.portfolio.plots.smoothed_holdings_performance_plot()
        fig.write_html(file=plots / "smooth_perf.html", auto_open=False, include_plotlyjs="cdn")

Exceptions

jquantstats.exceptions

Domain-specific exception types for the jquantstats package.

This module defines a hierarchy of exceptions that provide meaningful context when data-validation errors occur within the package.

All exceptions inherit from JQuantStatsError so callers can catch the entire family with a single except JQuantStatsError clause if they prefer.

Examples:

>>> raise MissingDateColumnError("prices")
Traceback (most recent call last):
    ...
jquantstats.exceptions.MissingDateColumnError: ...

BenchmarkAlignmentWarning

Bases: UserWarning

Emitted when aligning returns and benchmark drops rows from either side.

Returns and benchmark are aligned on their common dates with an inner join. Rows whose date appears in only one of the two frames are silently discarded by that join; this warning surfaces how many rows were lost so a partially overlapping benchmark cannot truncate the analysis unnoticed.

Suppress it once the overlap is understood::

import warnings
from jquantstats.exceptions import BenchmarkAlignmentWarning

warnings.filterwarnings("ignore", category=BenchmarkAlignmentWarning)
Source code in src/jquantstats/exceptions.py
class BenchmarkAlignmentWarning(UserWarning):
    """Emitted when aligning returns and benchmark drops rows from either side.

    Returns and benchmark are aligned on their common dates with an inner
    join.  Rows whose date appears in only one of the two frames are
    silently discarded by that join; this warning surfaces how many rows
    were lost so a partially overlapping benchmark cannot truncate the
    analysis unnoticed.

    Suppress it once the overlap is understood::

        import warnings
        from jquantstats.exceptions import BenchmarkAlignmentWarning

        warnings.filterwarnings("ignore", category=BenchmarkAlignmentWarning)
    """

IntegerIndexBoundError

Bases: JQuantStatsError, TypeError

Raised when a row-index bound is not an integer.

Parameters:

Name Type Description Default
param str

Name of the offending parameter (e.g. "start" or "end").

required
actual_type str

The type.__name__ of the value that was supplied.

required

Examples:

>>> raise IntegerIndexBoundError("start", "str")
Traceback (most recent call last):
    ...
jquantstats.exceptions.IntegerIndexBoundError: start must be an integer, got str.
Source code in src/jquantstats/exceptions.py
class IntegerIndexBoundError(JQuantStatsError, TypeError):
    """Raised when a row-index bound is not an integer.

    Args:
        param: Name of the offending parameter (e.g. ``"start"`` or ``"end"``).
        actual_type: The ``type.__name__`` of the value that was supplied.

    Examples:
        >>> raise IntegerIndexBoundError("start", "str")
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.IntegerIndexBoundError: start must be an integer, got str.
    """

    def __init__(self, param: str, actual_type: str) -> None:
        """Initialize with the parameter name and the offending type."""
        super().__init__(f"{param} must be an integer, got {actual_type}.")
        self.param = param
        self.actual_type = actual_type

__init__(param, actual_type)

Initialize with the parameter name and the offending type.

Source code in src/jquantstats/exceptions.py
def __init__(self, param: str, actual_type: str) -> None:
    """Initialize with the parameter name and the offending type."""
    super().__init__(f"{param} must be an integer, got {actual_type}.")
    self.param = param
    self.actual_type = actual_type

InvalidCashPositionTypeError

Bases: JQuantStatsError, TypeError

Raised when cashposition is not a polars.DataFrame.

Parameters:

Name Type Description Default
actual_type str

The type.__name__ of the value that was supplied.

required

Examples:

>>> raise InvalidCashPositionTypeError("dict")
Traceback (most recent call last):
    ...
jquantstats.exceptions.InvalidCashPositionTypeError: cashposition must be pl.DataFrame, got dict.
Source code in src/jquantstats/exceptions.py
class InvalidCashPositionTypeError(JQuantStatsError, TypeError):
    """Raised when ``cashposition`` is not a `polars.DataFrame`.

    Args:
        actual_type: The ``type.__name__`` of the value that was supplied.

    Examples:
        >>> raise InvalidCashPositionTypeError("dict")
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.InvalidCashPositionTypeError: cashposition must be pl.DataFrame, got dict.
    """

    def __init__(self, actual_type: str) -> None:
        """Initialize with the offending type name."""
        super().__init__(f"cashposition must be pl.DataFrame, got {actual_type}.")
        self.actual_type = actual_type

__init__(actual_type)

Initialize with the offending type name.

Source code in src/jquantstats/exceptions.py
def __init__(self, actual_type: str) -> None:
    """Initialize with the offending type name."""
    super().__init__(f"cashposition must be pl.DataFrame, got {actual_type}.")
    self.actual_type = actual_type

InvalidMaxBpsError

Bases: JQuantStatsError, ValueError

Raised when max_bps is not a positive integer.

Parameters:

Name Type Description Default
max_bps Any

The invalid value that was supplied.

required

Examples:

>>> raise InvalidMaxBpsError(0)
Traceback (most recent call last):
    ...
jquantstats.exceptions.InvalidMaxBpsError: max_bps must be a positive integer, got 0.
Source code in src/jquantstats/exceptions.py
class InvalidMaxBpsError(JQuantStatsError, ValueError):
    """Raised when ``max_bps`` is not a positive integer.

    Args:
        max_bps: The invalid value that was supplied.

    Examples:
        >>> raise InvalidMaxBpsError(0)
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.InvalidMaxBpsError: max_bps must be a positive integer, got 0.
    """

    def __init__(self, max_bps: Any) -> None:
        """Initialize with the offending value."""
        super().__init__(f"max_bps must be a positive integer, got {max_bps!r}.")
        self.max_bps = max_bps

__init__(max_bps)

Initialize with the offending value.

Source code in src/jquantstats/exceptions.py
def __init__(self, max_bps: Any) -> None:
    """Initialize with the offending value."""
    super().__init__(f"max_bps must be a positive integer, got {max_bps!r}.")
    self.max_bps = max_bps

InvalidPricesTypeError

Bases: JQuantStatsError, TypeError

Raised when prices is not a polars.DataFrame.

Parameters:

Name Type Description Default
actual_type str

The type.__name__ of the value that was supplied.

required

Examples:

>>> raise InvalidPricesTypeError("list")
Traceback (most recent call last):
    ...
jquantstats.exceptions.InvalidPricesTypeError: prices must be pl.DataFrame, got list.
Source code in src/jquantstats/exceptions.py
class InvalidPricesTypeError(JQuantStatsError, TypeError):
    """Raised when ``prices`` is not a `polars.DataFrame`.

    Args:
        actual_type: The ``type.__name__`` of the value that was supplied.

    Examples:
        >>> raise InvalidPricesTypeError("list")
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.InvalidPricesTypeError: prices must be pl.DataFrame, got list.
    """

    def __init__(self, actual_type: str) -> None:
        """Initialize with the offending type name."""
        super().__init__(f"prices must be pl.DataFrame, got {actual_type}.")
        self.actual_type = actual_type

__init__(actual_type)

Initialize with the offending type name.

Source code in src/jquantstats/exceptions.py
def __init__(self, actual_type: str) -> None:
    """Initialize with the offending type name."""
    super().__init__(f"prices must be pl.DataFrame, got {actual_type}.")
    self.actual_type = actual_type

InvalidTruncateBoundError

Bases: JQuantStatsError, ValueError

Raised when a truncation bound is neither a row index nor a usable date.

Covers a value of an unsupported type (a float, a bool) and a string that is not ISO-8601, both on an object that has a temporal index. An integer-indexed object raises IntegerIndexBoundError instead, since there the only legal bound is a row index.

Parameters:

Name Type Description Default
param str

Name of the offending parameter (e.g. "start" or "end").

required
value Any

The value that was supplied.

required

Examples:

>>> raise InvalidTruncateBoundError("start", "last tuesday")
Traceback (most recent call last):
    ...
jquantstats.exceptions.InvalidTruncateBoundError: start must be an int row index, a date/datetime, or an ISO-8601 string; got 'last tuesday'.
Source code in src/jquantstats/exceptions.py
class InvalidTruncateBoundError(JQuantStatsError, ValueError):
    """Raised when a truncation bound is neither a row index nor a usable date.

    Covers a value of an unsupported type (a float, a bool) and a string that
    is not ISO-8601, both on an object that *has* a temporal index.  An
    integer-indexed object raises `IntegerIndexBoundError` instead, since there
    the only legal bound is a row index.

    Args:
        param: Name of the offending parameter (e.g. ``"start"`` or ``"end"``).
        value: The value that was supplied.

    Examples:
        >>> raise InvalidTruncateBoundError("start", "last tuesday")
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.InvalidTruncateBoundError: start must be an int row index, a date/datetime, \
or an ISO-8601 string; got 'last tuesday'.
    """

    def __init__(self, param: str, value: Any) -> None:
        """Initialize with the parameter name and the offending value."""
        super().__init__(f"{param} must be an int row index, a date/datetime, or an ISO-8601 string; got {value!r}.")
        self.param = param
        self.value = value

__init__(param, value)

Initialize with the parameter name and the offending value.

Source code in src/jquantstats/exceptions.py
def __init__(self, param: str, value: Any) -> None:
    """Initialize with the parameter name and the offending value."""
    super().__init__(f"{param} must be an int row index, a date/datetime, or an ISO-8601 string; got {value!r}.")
    self.param = param
    self.value = value

JQuantStatsError

Bases: Exception

Base class for all JQuantStats domain errors.

Source code in src/jquantstats/exceptions.py
class JQuantStatsError(Exception):
    """Base class for all JQuantStats domain errors."""

MissingBackendError

Bases: JQuantStatsError, ImportError

Raised when a plotting backend is selected but its library is not installed.

Subclasses ImportError as well as JQuantStatsError so that callers already guarding optional rendering with except ImportError keep working unchanged.

Parameters:

Name Type Description Default
backend str

The backend that could not be loaded.

required
extra str

The packaging extra that installs it.

required

Examples:

>>> raise MissingBackendError("matplotlib", "mpl")
Traceback (most recent call last):
    ...
jquantstats.exceptions.MissingBackendError: the 'matplotlib' plot backend requires matplotlib...
Source code in src/jquantstats/exceptions.py
class MissingBackendError(JQuantStatsError, ImportError):
    """Raised when a plotting backend is selected but its library is not installed.

    Subclasses `ImportError` as well as `JQuantStatsError` so that callers
    already guarding optional rendering with ``except ImportError`` keep
    working unchanged.

    Args:
        backend: The backend that could not be loaded.
        extra: The packaging extra that installs it.

    Examples:
        >>> raise MissingBackendError("matplotlib", "mpl")
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.MissingBackendError: the 'matplotlib' plot backend requires matplotlib...
    """

    def __init__(self, backend: str, extra: str) -> None:
        """Initialize with the unavailable backend and the extra that provides it."""
        super().__init__(
            f"the {backend!r} plot backend requires {backend}, which is not installed. "
            f"Install it with: pip install 'jquantstats[{extra}]'"
        )
        self.backend = backend
        self.extra = extra

__init__(backend, extra)

Initialize with the unavailable backend and the extra that provides it.

Source code in src/jquantstats/exceptions.py
def __init__(self, backend: str, extra: str) -> None:
    """Initialize with the unavailable backend and the extra that provides it."""
    super().__init__(
        f"the {backend!r} plot backend requires {backend}, which is not installed. "
        f"Install it with: pip install 'jquantstats[{extra}]'"
    )
    self.backend = backend
    self.extra = extra

MissingDateColumnError

Bases: JQuantStatsError, ValueError

Raised when a required date column is absent from a DataFrame.

Parameters:

Name Type Description Default
frame_name str

Descriptive name of the frame missing the column (e.g. "prices").

required
column str | None

Name of the date column that was looked up (e.g. the date_col argument). When omitted, the column was being auto-detected rather than looked up by name.

None
available list[str] | None

Column names actually present in the frame, included in the error message to help diagnose the mismatch. Supplying them without a column selects the auto-detection wording — no temporal column was found to use as the date axis.

None

Examples:

>>> raise MissingDateColumnError("prices")
Traceback (most recent call last):
    ...
jquantstats.exceptions.MissingDateColumnError: ...
Source code in src/jquantstats/exceptions.py
class MissingDateColumnError(JQuantStatsError, ValueError):
    """Raised when a required date column is absent from a DataFrame.

    Args:
        frame_name: Descriptive name of the frame missing the column (e.g. ``"prices"``).
        column: Name of the date column that was looked up (e.g. the
            ``date_col`` argument). When omitted, the column was being
            auto-detected rather than looked up by name.
        available: Column names actually present in the frame, included in
            the error message to help diagnose the mismatch. Supplying them
            without a *column* selects the auto-detection wording — no
            temporal column was found to use as the date axis.

    Examples:
        >>> raise MissingDateColumnError("prices")  # doctest: +ELLIPSIS
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.MissingDateColumnError: ...
    """

    def __init__(self, frame_name: str, column: str | None = None, available: list[str] | None = None) -> None:
        """Initialize with the frame name and, optionally, the missing column and available columns."""
        available = [] if available is None else list(available)
        cols = ", ".join(f"'{c}'" for c in available) if available else ""
        if column is None and not cols:
            msg = f"DataFrame '{frame_name}' is missing the required 'date' column."
        else:
            lookup = (
                f"has no column '{column}' to use as the date column"
                if column is not None
                else "has no temporal column to use as the date column"
            )
            msg = (
                f"DataFrame '{frame_name}' {lookup}"
                + (f"; available columns: {cols}" if cols else "")
                + ". Pass date_col=<name of an existing column>."
            )
        super().__init__(msg)
        self.frame_name = frame_name
        self.column = column
        self.available = available

__init__(frame_name, column=None, available=None)

Initialize with the frame name and, optionally, the missing column and available columns.

Source code in src/jquantstats/exceptions.py
def __init__(self, frame_name: str, column: str | None = None, available: list[str] | None = None) -> None:
    """Initialize with the frame name and, optionally, the missing column and available columns."""
    available = [] if available is None else list(available)
    cols = ", ".join(f"'{c}'" for c in available) if available else ""
    if column is None and not cols:
        msg = f"DataFrame '{frame_name}' is missing the required 'date' column."
    else:
        lookup = (
            f"has no column '{column}' to use as the date column"
            if column is not None
            else "has no temporal column to use as the date column"
        )
        msg = (
            f"DataFrame '{frame_name}' {lookup}"
            + (f"; available columns: {cols}" if cols else "")
            + ". Pass date_col=<name of an existing column>."
        )
    super().__init__(msg)
    self.frame_name = frame_name
    self.column = column
    self.available = available

MissingReturnsColumnError

Bases: JQuantStatsError, ValueError

Raised when a frame handed to the returns bridge has no 'returns' column.

jquantstats.portfolio.Portfolio.as_data reads the return series from the 'returns' column and ignores every other column. A frame without one carries no return series at all, so it is rejected rather than silently analysed on whatever columns happen to be present.

Parameters:

Name Type Description Default
available list[str] | None

Column names actually present in the frame, included in the error message to help diagnose the mismatch.

None

Examples:

>>> raise MissingReturnsColumnError(["date", "profit"])
Traceback (most recent call last):
    ...
jquantstats.exceptions.MissingReturnsColumnError: ...
Source code in src/jquantstats/exceptions.py
class MissingReturnsColumnError(JQuantStatsError, ValueError):
    """Raised when a frame handed to the returns bridge has no ``'returns'`` column.

    `jquantstats.portfolio.Portfolio.as_data` reads the return series from the
    ``'returns'`` column and ignores every other column.  A frame without one
    carries no return series at all, so it is rejected rather than silently
    analysed on whatever columns happen to be present.

    Args:
        available: Column names actually present in the frame, included in the
            error message to help diagnose the mismatch.

    Examples:
        >>> raise MissingReturnsColumnError(["date", "profit"])  # doctest: +ELLIPSIS
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.MissingReturnsColumnError: ...
    """

    def __init__(self, available: list[str] | None = None) -> None:
        """Initialize with the column names present in the offending frame."""
        available = [] if available is None else list(available)
        cols = ", ".join(f"'{c}'" for c in available) if available else ""
        super().__init__(
            "DataFrame has no 'returns' column to bridge into Data"
            + (f"; available columns: {cols}" if cols else "")
            + ". Pass a frame produced by Portfolio.returns, cost_adjusted_returns "
            "or deduct_management_fee, or rename your return column to 'returns'."
        )
        self.available = available

__init__(available=None)

Initialize with the column names present in the offending frame.

Source code in src/jquantstats/exceptions.py
def __init__(self, available: list[str] | None = None) -> None:
    """Initialize with the column names present in the offending frame."""
    available = [] if available is None else list(available)
    cols = ", ".join(f"'{c}'" for c in available) if available else ""
    super().__init__(
        "DataFrame has no 'returns' column to bridge into Data"
        + (f"; available columns: {cols}" if cols else "")
        + ". Pass a frame produced by Portfolio.returns, cost_adjusted_returns "
        "or deduct_management_fee, or rename your return column to 'returns'."
    )
    self.available = available

MixedTruncateBoundsError

Bases: JQuantStatsError, TypeError

Raised when start and end mix a row index with a date.

The two describe different axes, so honouring one and ignoring the other would silently truncate to a range the caller never asked for.

Parameters:

Name Type Description Default
row_param str

Name of the parameter given as a row index.

required
date_param str

Name of the parameter given as a date.

required

Examples:

>>> raise MixedTruncateBoundsError("start", "end")
Traceback (most recent call last):
    ...
jquantstats.exceptions.MixedTruncateBoundsError: start and end must both be row indices or both be dates; got start as a row index and end as a date.
Source code in src/jquantstats/exceptions.py
class MixedTruncateBoundsError(JQuantStatsError, TypeError):
    """Raised when ``start`` and ``end`` mix a row index with a date.

    The two describe different axes, so honouring one and ignoring the other
    would silently truncate to a range the caller never asked for.

    Args:
        row_param: Name of the parameter given as a row index.
        date_param: Name of the parameter given as a date.

    Examples:
        >>> raise MixedTruncateBoundsError("start", "end")
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.MixedTruncateBoundsError: start and end must both be row indices or both be \
dates; got start as a row index and end as a date.
    """

    def __init__(self, row_param: str, date_param: str) -> None:
        """Initialize with the row-index and date parameter names."""
        super().__init__(
            f"start and end must both be row indices or both be dates; "
            f"got {row_param} as a row index and {date_param} as a date."
        )
        self.row_param = row_param
        self.date_param = date_param

__init__(row_param, date_param)

Initialize with the row-index and date parameter names.

Source code in src/jquantstats/exceptions.py
def __init__(self, row_param: str, date_param: str) -> None:
    """Initialize with the row-index and date parameter names."""
    super().__init__(
        f"start and end must both be row indices or both be dates; "
        f"got {row_param} as a row index and {date_param} as a date."
    )
    self.row_param = row_param
    self.date_param = date_param

MuSchemaError

Bases: JQuantStatsError, ValueError

Raised when a mu (expected-returns) frame doesn't match the portfolio's assets.

Parameters:

Name Type Description Default
missing list[str]

Portfolio asset columns absent from the mu frame.

required

Examples:

>>> raise MuSchemaError(["AAPL"])
Traceback (most recent call last):
    ...
jquantstats.exceptions.MuSchemaError: ...
Source code in src/jquantstats/exceptions.py
class MuSchemaError(JQuantStatsError, ValueError):
    """Raised when a ``mu`` (expected-returns) frame doesn't match the portfolio's assets.

    Args:
        missing: Portfolio asset columns absent from the mu frame.

    Examples:
        >>> raise MuSchemaError(["AAPL"])  # doctest: +ELLIPSIS
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.MuSchemaError: ...
    """

    def __init__(self, missing: list[str]) -> None:
        """Initialize with the asset columns missing from the mu frame."""
        cols = ", ".join(f"'{c}'" for c in missing)
        super().__init__(f"mu is missing expected-return columns for portfolio asset(s): {cols}.")
        self.missing = missing

__init__(missing)

Initialize with the asset columns missing from the mu frame.

Source code in src/jquantstats/exceptions.py
def __init__(self, missing: list[str]) -> None:
    """Initialize with the asset columns missing from the mu frame."""
    cols = ", ".join(f"'{c}'" for c in missing)
    super().__init__(f"mu is missing expected-return columns for portfolio asset(s): {cols}.")
    self.missing = missing

NegativeAnnualFeeError

Bases: JQuantStatsError, ValueError

Raised when an annual management fee is negative.

Parameters:

Name Type Description Default
annual_fee float

The negative fee value that was supplied.

required

Examples:

>>> raise NegativeAnnualFeeError(-0.01)
Traceback (most recent call last):
    ...
jquantstats.exceptions.NegativeAnnualFeeError: annual_fee must be non-negative, got -0.01.
Source code in src/jquantstats/exceptions.py
class NegativeAnnualFeeError(JQuantStatsError, ValueError):
    """Raised when an annual management fee is negative.

    Args:
        annual_fee: The negative fee value that was supplied.

    Examples:
        >>> raise NegativeAnnualFeeError(-0.01)
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.NegativeAnnualFeeError: annual_fee must be non-negative, got -0.01.
    """

    def __init__(self, annual_fee: float) -> None:
        """Initialize with the offending fee value."""
        super().__init__(f"annual_fee must be non-negative, got {annual_fee}.")
        self.annual_fee = annual_fee

__init__(annual_fee)

Initialize with the offending fee value.

Source code in src/jquantstats/exceptions.py
def __init__(self, annual_fee: float) -> None:
    """Initialize with the offending fee value."""
    super().__init__(f"annual_fee must be non-negative, got {annual_fee}.")
    self.annual_fee = annual_fee

NegativeCostBpsError

Bases: JQuantStatsError, ValueError

Raised when a trading cost in basis points is negative.

Parameters:

Name Type Description Default
cost_bps float

The negative cost value that was supplied.

required

Examples:

>>> raise NegativeCostBpsError(-1.0)
Traceback (most recent call last):
    ...
jquantstats.exceptions.NegativeCostBpsError: cost_bps must be non-negative, got -1.0.
Source code in src/jquantstats/exceptions.py
class NegativeCostBpsError(JQuantStatsError, ValueError):
    """Raised when a trading cost in basis points is negative.

    Args:
        cost_bps: The negative cost value that was supplied.

    Examples:
        >>> raise NegativeCostBpsError(-1.0)
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.NegativeCostBpsError: cost_bps must be non-negative, got -1.0.
    """

    def __init__(self, cost_bps: float) -> None:
        """Initialize with the offending cost value."""
        super().__init__(f"cost_bps must be non-negative, got {cost_bps}.")
        self.cost_bps = cost_bps

__init__(cost_bps)

Initialize with the offending cost value.

Source code in src/jquantstats/exceptions.py
def __init__(self, cost_bps: float) -> None:
    """Initialize with the offending cost value."""
    super().__init__(f"cost_bps must be non-negative, got {cost_bps}.")
    self.cost_bps = cost_bps

NoAssetColumnsError

Bases: JQuantStatsError, ValueError

Raised when a DataFrame contains no numeric asset columns to aggregate.

Parameters:

Name Type Description Default
frame_name str

Descriptive name of the frame without asset columns (e.g. "profits").

required

Examples:

>>> raise NoAssetColumnsError("profits")
Traceback (most recent call last):
    ...
jquantstats.exceptions.NoAssetColumnsError: ...
Source code in src/jquantstats/exceptions.py
class NoAssetColumnsError(JQuantStatsError, ValueError):
    """Raised when a DataFrame contains no numeric asset columns to aggregate.

    Args:
        frame_name: Descriptive name of the frame without asset columns (e.g. ``"profits"``).

    Examples:
        >>> raise NoAssetColumnsError("profits")  # doctest: +ELLIPSIS
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.NoAssetColumnsError: ...
    """

    def __init__(self, frame_name: str) -> None:
        """Initialize with the name of the frame lacking asset columns."""
        super().__init__(
            f"DataFrame '{frame_name}' contains no numeric asset columns; "
            f"at least one numeric column besides 'date' is required."
        )
        self.frame_name = frame_name

__init__(frame_name)

Initialize with the name of the frame lacking asset columns.

Source code in src/jquantstats/exceptions.py
def __init__(self, frame_name: str) -> None:
    """Initialize with the name of the frame lacking asset columns."""
    super().__init__(
        f"DataFrame '{frame_name}' contains no numeric asset columns; "
        f"at least one numeric column besides 'date' is required."
    )
    self.frame_name = frame_name

NoBenchmarkError

Bases: JQuantStatsError, AttributeError

Raised when a benchmark-dependent statistic is requested without a benchmark.

Subclasses AttributeError so existing callers that catch AttributeError for the no-benchmark path keep working unchanged.

Examples:

>>> raise NoBenchmarkError()
Traceback (most recent call last):
    ...
jquantstats.exceptions.NoBenchmarkError: No benchmark data available
Source code in src/jquantstats/exceptions.py
class NoBenchmarkError(JQuantStatsError, AttributeError):
    """Raised when a benchmark-dependent statistic is requested without a benchmark.

    Subclasses `AttributeError` so existing callers that catch
    ``AttributeError`` for the no-benchmark path keep working unchanged.

    Examples:
        >>> raise NoBenchmarkError()
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.NoBenchmarkError: No benchmark data available
    """

    def __init__(self) -> None:
        """Initialize with the fixed no-benchmark message."""
        super().__init__("No benchmark data available")

__init__()

Initialize with the fixed no-benchmark message.

Source code in src/jquantstats/exceptions.py
def __init__(self) -> None:
    """Initialize with the fixed no-benchmark message."""
    super().__init__("No benchmark data available")

NonPositiveAumError

Bases: JQuantStatsError, ValueError

Raised when aum is not strictly positive.

Parameters:

Name Type Description Default
aum float

The non-positive value that was supplied.

required

Examples:

>>> raise NonPositiveAumError(0.0)
Traceback (most recent call last):
    ...
jquantstats.exceptions.NonPositiveAumError: aum must be strictly positive, got 0.0.
Source code in src/jquantstats/exceptions.py
class NonPositiveAumError(JQuantStatsError, ValueError):
    """Raised when ``aum`` is not strictly positive.

    Args:
        aum: The non-positive value that was supplied.

    Examples:
        >>> raise NonPositiveAumError(0.0)
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.NonPositiveAumError: aum must be strictly positive, got 0.0.
    """

    def __init__(self, aum: float) -> None:
        """Initialize with the offending aum value."""
        super().__init__(f"aum must be strictly positive, got {aum}.")
        self.aum = aum

__init__(aum)

Initialize with the offending aum value.

Source code in src/jquantstats/exceptions.py
def __init__(self, aum: float) -> None:
    """Initialize with the offending aum value."""
    super().__init__(f"aum must be strictly positive, got {aum}.")
    self.aum = aum

NonPositivePeriodsPerYearError

Bases: JQuantStatsError, ValueError

Raised when periods_per_year is not strictly positive.

Examples:

>>> raise NonPositivePeriodsPerYearError()
Traceback (most recent call last):
    ...
jquantstats.exceptions.NonPositivePeriodsPerYearError: periods_per_year must be positive
Source code in src/jquantstats/exceptions.py
class NonPositivePeriodsPerYearError(JQuantStatsError, ValueError):
    """Raised when ``periods_per_year`` is not strictly positive.

    Examples:
        >>> raise NonPositivePeriodsPerYearError()
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.NonPositivePeriodsPerYearError: periods_per_year must be positive
    """

    def __init__(self) -> None:
        """Initialize with the fixed non-positive periods-per-year message."""
        super().__init__("periods_per_year must be positive")

__init__()

Initialize with the fixed non-positive periods-per-year message.

Source code in src/jquantstats/exceptions.py
def __init__(self) -> None:
    """Initialize with the fixed non-positive periods-per-year message."""
    super().__init__("periods_per_year must be positive")

NonPositiveWindowError

Bases: JQuantStatsError, ValueError

Raised when a rolling-window size is not a positive integer.

Parameters:

Name Type Description Default
param str

Name of the offending parameter (e.g. "window" or "rolling_period").

required

Examples:

>>> raise NonPositiveWindowError("window")
Traceback (most recent call last):
    ...
jquantstats.exceptions.NonPositiveWindowError: window must be a positive integer
Source code in src/jquantstats/exceptions.py
class NonPositiveWindowError(JQuantStatsError, ValueError):
    """Raised when a rolling-window size is not a positive integer.

    Args:
        param: Name of the offending parameter (e.g. ``"window"`` or
            ``"rolling_period"``).

    Examples:
        >>> raise NonPositiveWindowError("window")
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.NonPositiveWindowError: window must be a positive integer
    """

    def __init__(self, param: str) -> None:
        """Initialize with the name of the offending window parameter."""
        super().__init__(f"{param} must be a positive integer")
        self.param = param

__init__(param)

Initialize with the name of the offending window parameter.

Source code in src/jquantstats/exceptions.py
def __init__(self, param: str) -> None:
    """Initialize with the name of the offending window parameter."""
    super().__init__(f"{param} must be a positive integer")
    self.param = param

NullsInReturnsError

Bases: JQuantStatsError, ValueError

Raised when null values are detected in returns (or benchmark) data.

Polars propagates null through calculations whereas pandas silently drops NaN. Leaving nulls in place will cause most statistics to return null instead of a numeric result.

Use the null_strategy parameter on from_returns or from_prices to handle nulls automatically, or clean the data before construction.

Parameters:

Name Type Description Default
frame_name str

Descriptive name of the frame that contains nulls (e.g. "returns" or "benchmark").

required
columns list[str]

Names of the columns that contain at least one null.

required

Examples:

>>> raise NullsInReturnsError("returns", ["Asset1", "Asset2"])
Traceback (most recent call last):
    ...
jquantstats.exceptions.NullsInReturnsError: ...
Source code in src/jquantstats/exceptions.py
class NullsInReturnsError(JQuantStatsError, ValueError):
    """Raised when null values are detected in returns (or benchmark) data.

    Polars propagates ``null`` through calculations whereas pandas silently
    drops ``NaN``.  Leaving nulls in place will cause most statistics to
    return ``null`` instead of a numeric result.

    Use the ``null_strategy`` parameter on `from_returns`
    or `from_prices` to handle nulls automatically, or
    clean the data before construction.

    Args:
        frame_name: Descriptive name of the frame that contains nulls
            (e.g. ``"returns"`` or ``"benchmark"``).
        columns: Names of the columns that contain at least one null.

    Examples:
        >>> raise NullsInReturnsError("returns", ["Asset1", "Asset2"])
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.NullsInReturnsError: ...
    """

    def __init__(self, frame_name: str, columns: list[str]) -> None:
        """Initialize with the frame name and the columns that contain nulls."""
        cols_str = ", ".join(f"'{c}'" for c in columns)
        super().__init__(
            f"DataFrame '{frame_name}' contains null values in column(s): {cols_str}. "
            f"Pass null_strategy='drop' or null_strategy='forward_fill' to handle nulls "
            f"automatically, or clean the data before construction."
        )
        self.frame_name = frame_name
        self.columns = columns

__init__(frame_name, columns)

Initialize with the frame name and the columns that contain nulls.

Source code in src/jquantstats/exceptions.py
def __init__(self, frame_name: str, columns: list[str]) -> None:
    """Initialize with the frame name and the columns that contain nulls."""
    cols_str = ", ".join(f"'{c}'" for c in columns)
    super().__init__(
        f"DataFrame '{frame_name}' contains null values in column(s): {cols_str}. "
        f"Pass null_strategy='drop' or null_strategy='forward_fill' to handle nulls "
        f"automatically, or clean the data before construction."
    )
    self.frame_name = frame_name
    self.columns = columns

PositionExprColumnError

Bases: JQuantStatsError, ValueError

Raised when a position expression creates columns that do not exist in prices.

Position expressions (cash_position, position, risk_position) are evaluated against the prices frame and must overwrite existing asset columns. An expression that creates a new column (e.g. via .alias) leaves the original asset columns untouched, which would silently treat raw prices as positions.

Parameters:

Name Type Description Default
param str

Name of the offending parameter (e.g. "cash_position").

required
extra list[str]

Column names created by the expression that are absent from prices.

required

Examples:

>>> raise PositionExprColumnError("cash_position", ["A2"])
Traceback (most recent call last):
    ...
jquantstats.exceptions.PositionExprColumnError: ...
Source code in src/jquantstats/exceptions.py
class PositionExprColumnError(JQuantStatsError, ValueError):
    """Raised when a position expression creates columns that do not exist in prices.

    Position expressions (``cash_position``, ``position``, ``risk_position``)
    are evaluated against the prices frame and must overwrite existing asset
    columns. An expression that creates a *new* column (e.g. via ``.alias``)
    leaves the original asset columns untouched, which would silently treat
    raw prices as positions.

    Args:
        param: Name of the offending parameter (e.g. ``"cash_position"``).
        extra: Column names created by the expression that are absent from prices.

    Examples:
        >>> raise PositionExprColumnError("cash_position", ["A2"])  # doctest: +ELLIPSIS
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.PositionExprColumnError: ...
    """

    def __init__(self, param: str, extra: list[str]) -> None:
        """Initialize with the parameter name and the unexpected columns it created."""
        cols = ", ".join(f"'{c}'" for c in extra)
        super().__init__(
            f"{param} expression created new column(s) {cols} that do not exist in prices. "
            f"Expressions must overwrite existing asset columns (e.g. pl.col('A') * 2); "
            f"asset columns the expression does not overwrite keep their raw price values."
        )
        self.param = param
        self.extra = list(extra)

__init__(param, extra)

Initialize with the parameter name and the unexpected columns it created.

Source code in src/jquantstats/exceptions.py
def __init__(self, param: str, extra: list[str]) -> None:
    """Initialize with the parameter name and the unexpected columns it created."""
    cols = ", ".join(f"'{c}'" for c in extra)
    super().__init__(
        f"{param} expression created new column(s) {cols} that do not exist in prices. "
        f"Expressions must overwrite existing asset columns (e.g. pl.col('A') * 2); "
        f"asset columns the expression does not overwrite keep their raw price values."
    )
    self.param = param
    self.extra = list(extra)

RowCountMismatchError

Bases: JQuantStatsError, ValueError

Raised when prices and cashposition have different numbers of rows.

Parameters:

Name Type Description Default
prices_rows int

Number of rows in the prices DataFrame.

required
cashposition_rows int

Number of rows in the cashposition DataFrame.

required

Examples:

>>> raise RowCountMismatchError(10, 9)
Traceback (most recent call last):
    ...
jquantstats.exceptions.RowCountMismatchError: ...
Source code in src/jquantstats/exceptions.py
class RowCountMismatchError(JQuantStatsError, ValueError):
    """Raised when ``prices`` and ``cashposition`` have different numbers of rows.

    Args:
        prices_rows: Number of rows in the prices DataFrame.
        cashposition_rows: Number of rows in the cashposition DataFrame.

    Examples:
        >>> raise RowCountMismatchError(10, 9)  # doctest: +ELLIPSIS
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.RowCountMismatchError: ...
    """

    def __init__(self, prices_rows: int, cashposition_rows: int) -> None:
        """Initialize with the row counts of the two mismatched DataFrames."""
        super().__init__(
            f"cashposition and prices must have the same number of rows, "
            f"got cashposition={cashposition_rows} and prices={prices_rows}."
        )
        self.prices_rows = prices_rows
        self.cashposition_rows = cashposition_rows

__init__(prices_rows, cashposition_rows)

Initialize with the row counts of the two mismatched DataFrames.

Source code in src/jquantstats/exceptions.py
def __init__(self, prices_rows: int, cashposition_rows: int) -> None:
    """Initialize with the row counts of the two mismatched DataFrames."""
    super().__init__(
        f"cashposition and prices must have the same number of rows, "
        f"got cashposition={cashposition_rows} and prices={prices_rows}."
    )
    self.prices_rows = prices_rows
    self.cashposition_rows = cashposition_rows

UncleanSeriesError

Bases: JQuantStatsError, ValueError

Raised when a derived series contains null or non-finite values.

Parameters:

Name Type Description Default
name str

Name of the offending series (may be empty when unknown).

required
reason str

Either "null" or "non-finite".

required

Examples:

>>> raise UncleanSeriesError("profit", "null")
Traceback (most recent call last):
    ...
jquantstats.exceptions.UncleanSeriesError: ...
Source code in src/jquantstats/exceptions.py
class UncleanSeriesError(JQuantStatsError, ValueError):
    """Raised when a derived series contains null or non-finite values.

    Args:
        name: Name of the offending series (may be empty when unknown).
        reason: Either ``"null"`` or ``"non-finite"``.

    Examples:
        >>> raise UncleanSeriesError("profit", "null")  # doctest: +ELLIPSIS
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.UncleanSeriesError: ...
    """

    def __init__(self, name: str, reason: str) -> None:
        """Initialize with the series name and the kind of dirty value found."""
        label = f"series '{name}'" if name else "series"
        super().__init__(
            f"{label} contains {reason} values; inputs must produce a clean, finite series. "
            f"Check prices and positions for gaps or zero/negative prices."
        )
        self.name = name
        self.reason = reason

__init__(name, reason)

Initialize with the series name and the kind of dirty value found.

Source code in src/jquantstats/exceptions.py
def __init__(self, name: str, reason: str) -> None:
    """Initialize with the series name and the kind of dirty value found."""
    label = f"series '{name}'" if name else "series"
    super().__init__(
        f"{label} contains {reason} values; inputs must produce a clean, finite series. "
        f"Check prices and positions for gaps or zero/negative prices."
    )
    self.name = name
    self.reason = reason

UnknownPlotBackendError

Bases: JQuantStatsError, ValueError

Raised when an unrecognised plotting backend is selected.

Parameters:

Name Type Description Default
backend str

The rejected backend name.

required
supported list[str]

The backend names that are accepted, in display order.

required

Examples:

>>> raise UnknownPlotBackendError("ggplot", ["matplotlib", "plotly"])
Traceback (most recent call last):
    ...
jquantstats.exceptions.UnknownPlotBackendError: unknown plot backend 'ggplot'; ...
Source code in src/jquantstats/exceptions.py
class UnknownPlotBackendError(JQuantStatsError, ValueError):
    """Raised when an unrecognised plotting backend is selected.

    Args:
        backend: The rejected backend name.
        supported: The backend names that are accepted, in display order.

    Examples:
        >>> raise UnknownPlotBackendError("ggplot", ["matplotlib", "plotly"])
        Traceback (most recent call last):
            ...
        jquantstats.exceptions.UnknownPlotBackendError: unknown plot backend 'ggplot'; ...
    """

    def __init__(self, backend: str, supported: list[str]) -> None:
        """Initialize with the rejected backend and the accepted alternatives."""
        expected = ", ".join(repr(name) for name in supported)
        super().__init__(f"unknown plot backend {backend!r}; expected one of {expected}")
        self.backend = backend
        self.supported = supported

__init__(backend, supported)

Initialize with the rejected backend and the accepted alternatives.

Source code in src/jquantstats/exceptions.py
def __init__(self, backend: str, supported: list[str]) -> None:
    """Initialize with the rejected backend and the accepted alternatives."""
    expected = ", ".join(repr(name) for name in supported)
    super().__init__(f"unknown plot backend {backend!r}; expected one of {expected}")
    self.backend = backend
    self.supported = supported