at-research-tools 0.1.2
Add your description here
Requires Python: >=3.13

# at-research-tools

Atium Research tools for mean-variance optimal portfolio backtesting with risk-targeted optimization.

## Overview

`at-research-tools` is a Python package for quantitative portfolio research and backtesting. It provides tools to construct optimal portfolios using mean-variance optimization with customizable constraints and risk targets. The package implements a factor model-based covariance estimation and uses CVXPY for efficient convex optimization.

## Features

- **Risk-Targeted Portfolio Optimization**: Automatically calibrate the risk aversion parameter (lambda) to achieve a target tracking error
- **Factor Model Covariance**: Build covariance matrices from factor loadings, factor covariances, and idiosyncratic volatilities
- **Flexible Constraints**: Extensible constraint system supporting fully invested, long-only, and custom constraints
- **Backtesting Framework**: Run multi-period backtests with daily rebalancing
- **Active Risk Management**: Calculate and target active risk (tracking error) relative to a benchmark

## Installation

```bash
pip install at-research-tools
```

## Quick Start

Here's a complete example demonstrating how to build a momentum-based portfolio with risk-targeted optimization:

```python
from at_research_tools.constraints import FullyInvested, LongOnly
from at_research_tools.backtester import run_backtest
import at_data_tools as adt
import datetime as dt
import polars as pl

# Define backtest parameters
start = dt.date(2024, 12, 1)
end = dt.date(2025, 12, 31)
target_active_risk = 0.05  # 5% tracking error target

# Set up constraints
constraints = [
    FullyInvested(),  # Weights sum to 1
    LongOnly()        # No short positions
]

# Load market data
universe = adt.load_universe(start, end)
returns = adt.load_stock_returns(start, end)
idio_vol = adt.load_idio_vol(start, end)
factor_loadings = adt.load_factor_loadings(start, end)
factor_covariances = adt.load_factor_covariances(start, end)
benchmark_weights = adt.load_benchmark_weights(start, end)

# Combine data
data = (
    universe
    .join(returns, on=['date', 'ticker'], how='left')
    .join(idio_vol, on=['date', 'ticker'], how='left')
)

# Create momentum signal
signals = (
    data
    .sort('ticker', 'date')
    .with_columns(
        pl.col('return')
        .log1p()
        .rolling_sum(230)  # ~1 year momentum
        .shift(21)          # 1-month lag
        .over('ticker')
        .alias('momentum')
    )
)

# Filter for valid data
filtered = (
    signals
    .filter(
        pl.col('momentum').is_not_null(),
        pl.col('idio_vol').is_not_null()
    )
)

# Standardize scores and convert to alphas
scores = (
    filtered
    .with_columns(
        pl.col('momentum')
        .sub(pl.col('momentum').mean())
        .truediv(pl.col('momentum').std())
        .over('date')
        .alias('score')
    )
)

alphas = (
    scores
    .with_columns(
        pl.lit(0.05)
        .mul('idio_vol')
        .mul('score')
        .alias('alpha')
    )
)

# Run backtest
weights = run_backtest(
    alphas=alphas,
    factor_loadings=factor_loadings,
    factor_covariances=factor_covariances,
    idio_vol=idio_vol,
    benchmark_weights=benchmark_weights,
    constraints=constraints,
    target_active_risk=target_active_risk
)

print(weights)
```

## Core Components

### Backtester

The `run_backtest` function orchestrates the entire backtesting process:

```python
from at_research_tools.backtester import run_backtest

weights = run_backtest(
    alphas=alphas,                          # Expected returns
    factor_loadings=factor_loadings,        # Factor exposures
    factor_covariances=factor_covariances,  # Factor covariances
    idio_vol=idio_vol,                      # Idiosyncratic volatility
    benchmark_weights=benchmark_weights,    # Benchmark for active risk
    constraints=constraints,                # Portfolio constraints
    target_active_risk=0.05                 # 5% tracking error target
)
```

### Covariance Matrix

Build covariance matrices using factor model decomposition:

```python
from at_research_tools.covariance_matrix import build_covariance_matrix

cov_matrix = build_covariance_matrix(
    tickers=tickers,
    factor_loadings=factor_loadings,
    factor_covariances=factor_covariances,
    idio_vol=idio_vol
)
```

The covariance matrix is constructed using: `Cov = B @ F @ B^T + D^2`

### Portfolio Optimization

Optimize portfolio weights with custom constraints:

```python
from at_research_tools.portfolio import optimize_portfolio, optimize_lambda

# Find optimal lambda for target risk
lambda_, achieved_risk = optimize_lambda(
    tickers=tickers,
    alphas=alphas,
    covariance_matrix=cov_matrix,
    benchmark_weights=benchmark_weights,
    constraints=constraints,
    target_active_risk=0.05
)

# Optimize portfolio with calibrated lambda
weights = optimize_portfolio(
    tickers=tickers,
    alphas=alphas,
    covariance_matrix=cov_matrix,
    constraints=constraints,
    lambda_=lambda_
)
```

### Constraints

Create custom constraints by extending the `Constraint` base class:

```python
from at_research_tools.constraints import Constraint, FullyInvested, LongOnly
import cvxpy as cp
from dataclasses import dataclass

@dataclass
class MaxPositionSize(Constraint):
    """Limit maximum weight per asset."""
    max_weight: float = 0.05

    def build(self, weights, tickers):
        return [weights <= self.max_weight]

    @property
    def name(self):
        return f"MaxPositionSize({self.max_weight})"

# Use in optimization
constraints = [
    FullyInvested(),
    LongOnly(),
    MaxPositionSize(max_weight=0.05)
]
```

## Data Format

All DataFrames should follow these schemas:

**alphas**: `date`, `ticker`, `alpha`
**factor_loadings**: `date`, `ticker`, `factor`, `loading`
**factor_covariances**: `date`, `factor_1`, `factor_2`, `covariance`
**idio_vol**: `date`, `ticker`, `idio_vol`
**benchmark_weights**: `date`, `ticker`, `weight`