Metadata-Version: 2.5
Name: malatium
Version: 0.1.0
Summary: Volatility portfolio backtesting and optimization in dollar vega, built on Polars and CVXPY.
Project-URL: Repository, https://github.com/Atium-Research/malatium
Requires-Python: >=3.13
Requires-Dist: cvxpy>=1.6
Requires-Dist: dataframely>=2.7
Requires-Dist: numpy>=2.0
Requires-Dist: polars>=1.30
Provides-Extra: plot
Requires-Dist: matplotlib>=3.9; extra == 'plot'
Description-Content-Type: text/markdown

# Malatium

Volatility portfolio backtesting and optimization in dollar vega, built on [Polars](https://pola.rs/) and [CVXPY](https://www.cvxpy.org/). Sibling of [atium](https://github.com/Atium-Research/atium) with vega in place of capital.

Malatium is data-agnostic. It takes frames in through providers and never reads a store: the reference-return panel it prices a book off, the risk model tables and the scores or alphas all come from the caller (in the Atium stack, from `ml-data-pipelines` through `ml-data-access`).

## Installation

```bash
pip install malatium
```

## Units

Every position is a number of **units** of a reference instrument per name (in the Atium stack, one delta-hedged ATM straddle rolled on a fixed rule). A unit is marked by its **dollar vega**, the dollars of P&L per one-point move in implied vol. The return of a unit is its daily P&L divided by its dollar vega at inception, `pnl_per_vega`. Strategy weights are fractions of a **gross vega budget**; the backtester turns a weight into units by multiplying by the budget and dividing by the unit's dollar vega at today's close.

## Quick start

```python
import datetime as dt

from malatium.backtester import Backtester
from malatium.optimizer import MVO, GrossCap, MaxUtility, NetVegaNeutral, TurnoverPenalty
from malatium.providers import PanelProvider, TradingCalendar
from malatium.results import BacktestResults
from malatium.risk_model import FactorRiskModelConstructor
from malatium.strategy import OptimizationStrategy, QuantileSpreadStrategy

# Frames in the schemas of `malatium.schemas`, loaded however you like.
calendar = TradingCalendar(sessions)
reference = PanelProvider(reference_returns_df)

# A rank book: long the cheapest decile of scores, short the richest.
rank_book = QuantileSpreadStrategy(
    PanelProvider(scores_df), universe=PanelProvider(universe_df), quantile=0.1
)

# A mean-variance book: alphas against a factor risk model.
risk_model = FactorRiskModelConstructor(
    PanelProvider(factor_loadings_df),
    PanelProvider(factor_covariances_df),
    PanelProvider(idio_vol_df),
)
optimizer = MVO(
    objectives=[MaxUtility(risk_aversion=0.2), TurnoverPenalty(cost=0.1)],
    constraints=[NetVegaNeutral(0.025), GrossCap(1.0)],
)
mvo_book = OptimizationStrategy(PanelProvider(alphas_df), risk_model, optimizer)

records_df = Backtester().run(
    calendar,
    reference,
    mvo_book,
    start=dt.date(2018, 7, 2),
    end=dt.date(2025, 6, 30),
    gross_vega=20_000.0,
    rebalance_frequency="weekly",
)
results = BacktestResults(records_df)
results.summary()
results.factor_regression(factor_returns_df)
BacktestResults.decile_table(scores_df, reference_returns_df, horizon_days=60)
```

## What is here

| module | holds |
| --- | --- |
| `schemas.py`, `types.py` | dataframely schemas for every frame in and out |
| `data.py` | provider protocols: `get(date_)` for scores, alphas, universe, reference returns, risk model tables |
| `providers.py` | `TradingCalendar`, `PanelProvider` over frames in memory |
| `strategy.py` | `Strategy`, `QuantileSpreadStrategy`, `OptimizationStrategy` |
| `optimizer/` | `MVO`; objectives `MaxUtility`, `TurnoverPenalty`; constraints `NetVegaNeutral`, `FactorNeutral`, `PerNameCap`, `GrossShortCap`, `GrossCap` |
| `risk_model/` | `RiskModel`, `FactorRiskModel` (Sigma = B F B' + D²), `FactorRiskModelConstructor` |
| `backtester.py` | the daily loop over the reference-return panel |
| `results.py` | `BacktestResults`: summary, factor regression, decile table, plots (`pip install 'malatium[plot]'`) |

## Reference returns

`ReferenceReturnsSchema` is the contract between the data and the engine. Per `(date, symbol)`:

| column | meaning |
| --- | --- |
| `pnl_per_vega` | the unit's P&L today per dollar of its inception vega |
| `cost_per_vega` | rolling and hedging cost today, same scale |
| `exit_cost_per_vega` | half-spread to close the unit today, same scale |
| `dollar_vega` | the unit's dollar vega at today's close |
| `entry_vega` | inception dollar vega of the unit held into today |
| `event` | `open`, `roll`, `forced_close` or empty |

Between rebalances the backtester holds a constant number of units, so its P&L is `units × entry_vega × pnl_per_vega` and its cost `|units| × entry_vega × cost_per_vega`; a rebalance trades `|Δunits| × half-spread`. Costs are charged at `cost_fraction` of those amounts, zero by default.

## Development

```bash
uv sync --all-extras
uv run pytest
uv run ruff check . && uv run ruff format .
```
