Skip to content

breaded-xyz/alphavec

Repository files navigation

Alphavec

CI Upload Python Package

Disclaimer

The content provided in this project is for informational purposes only and does not constitute financial advice. This information should not be construed as professional financial advice, and it is recommended to consult with a qualified financial advisor before making any financial decisions.

No liability is accepted for any losses or damages incurred as a result of acting or refraining from action based on the information provided in this project. Use this information at your own risk.

          $$\           $$\                                               
          $$ |          $$ |                                              
 $$$$$$\  $$ | $$$$$$\  $$$$$$$\   $$$$$$\ $$\    $$\  $$$$$$\   $$$$$$$\ 
 \____$$\ $$ |$$  __$$\ $$  __$$\  \____$$\\$$\  $$  |$$  __$$\ $$  _____|
 $$$$$$$ |$$ |$$ /  $$ |$$ |  $$ | $$$$$$$ |\$$\$$  / $$$$$$$$ |$$ /      
$$  __$$ |$$ |$$ |  $$ |$$ |  $$ |$$  __$$ | \$$$  /  $$   ____|$$ |      
\$$$$$$$ |$$ |$$$$$$$  |$$ |  $$ |\$$$$$$$ |  \$  /   \$$$$$$$\ \$$$$$$$\ 
 \_______|\__|$$  ____/ \__|  \__| \_______|   \_/     \_______| \_______|
              $$ |                                                        
              $$ |                                                        
              \__|                                                                                                         

Alphavec is a lightning fast, minimalist, cost-aware vectorized backtest engine inspired by the guys at RobotWealth.

The backtest input is the natural output of a typical quant research process - a time series of portfolio weights. You simply provide a dataframe of weights and a dataframe of close prices and order prices, along with some optional cost parameters and the backtest returns a streamlined performance report with insight into the key metrics.

alphavec has first-class support for simulating leveraged perptual futures strategies using a small, fast, verifiable simulation core.

Rationale

Alphavec is an antidote to the various bloated and complex backtest frameworks.

To validate ideas all you really need is...

weights * returns.shift(-1)

The goal was to add just enough extra complexity to this basic formula to support sound development of cost-aware systematic trading strategies.

Install

Requires Python >=3.10

pip install alphavec

  • From source:
    • python3 -m venv .venv
    • ./.venv/bin/pip install -e .
  • For development:
    • ./.venv/bin/pip install -e ".[dev]"

Usage

Notes

  • Simulates cross‑margin (cash pooling) with unlimited leverage and borrowing (no liquidations or margin calls).
  • Orders execute at exec_prices plus slippage and fees.
  • Funding applies per period using signed funding_rates, +ve rate shorts earn, longs pay, and vice versa for a -ve rate.
  • NaNs in exec_prices or close_prices imply the asset is not tradable that period.
  • NaNs in funding_rates are treated as 0, and funding is always 0 when close_prices is NaN.
  • Positions will always be closed if target weight is zero, regardless of minimum notional filter.
  • Use SimConfig.start_period/end_period to slice inputs before simulating: str uses loc, int uses iloc.

Simulation

simulate() runs a cross‑margin perpetual futures backtest from target portfolio weights.

Key inputs:

  • weights: pandas DataFrame with a DatetimeIndex and columns for each asset. Values are decimal percentage target weights (1.0 = 100% equity invested). Positive = long, negative = short. Weights may sum greater than 1 at a time period for leverage.
  • close_prices, exec_prices, funding_rates (optional): same shape/index/columns as weights.

Returns:

  • returns: period returns as a pandas Series.
  • metrics: key performance metrics as a pandas DataFrame with Value and Note columns.

Example:

See examples/simulate.ipynb

import pandas as pd
from alphavec import MarketData, SimConfig, simulate, tearsheet

weights = pd.DataFrame({"BTC": [1, 1, 1]}, index=pd.date_range("2024-01-01", periods=3, freq="1D"))
close_prices = pd.DataFrame({"BTC": [100, 105, 110]}, index=weights.index)
exec_prices = close_prices.shift(1).fillna(close_prices.iloc[0])

result = simulate(
    weights=weights,
    market=MarketData(close_prices=close_prices, exec_prices=exec_prices, funding_rates=None),
    config=SimConfig(
        benchmark_asset="BTC",
        order_notional_min=10,
        fee_rate=0.00025,       # 2.5 bps per trade
        slippage_rate=0.001,    # 10 bps per trade
        start_period="2023-01-01",
        end_period="2025-12-06",
        init_cash=10_000,
        freq_rule="1D",
        trading_days_year=365,
        risk_free_rate=0.03,
    ),
)
html_str = tearsheet(
    sim_result=result,
    output_path="tearsheet.html",
    smooth_periods=30,
)

Parameter Search

grid_search() wraps simulate() and runs grid search over 1D or 2D parameter spaces where the objective is a simulation metric.

See examples/search.ipynb

from alphavec import grid_search, Metrics, MarketData, SimConfig

# Simple 1D search with dict syntax (most concise)
results = grid_search(
    generate_weights=generate_weights,  # def generate_weights(params: Mapping) -> pd.DataFrame
    objective_metric=Metrics.SHARPE,  # Type-safe metric names
    param_grid={"lookback": [5, 10, 20, 40]},  # Dict syntax - no Grid2D needed!
    market=MarketData(close_prices=close_prices, exec_prices=exec_prices, funding_rates=None),
    config=SimConfig(),
)

# 2D search with multiple grids
results = grid_search(
    generate_weights=generate_weights,
    param_grids=[
        {"lookback": [5, 10, 20], "leverage": [0.5, 1.0, 2.0]},  # 2D dict
        {"lookback": [5, 10, 20], "smooth_span": [1, 5, 10]},   # Another 2D search
    ],
    market=market,
    config=config,
)

# Minimize risk metrics
results = grid_search(
    generate_weights=generate_weights,
    objective_metric=Metrics.MAX_DRAWDOWN,
    maximize=False,  # Minimize drawdown instead of maximize
    param_grid={"lookback": [10, 20, 40]},
    market=market,
)

# Access results
results.table          # Full results DataFrame
results.best.params    # Best parameters
results.top(5)         # Top 5 parameter combinations
results.summary()      # Summary statistics by grid
results.plot()         # Smart plot: line for 1D, heatmap for 2D

# Extract metrics (unified API)
results.metric_value(MetricKey.ANNUALIZED_SHARPE)
results.available_metrics("Performance")
results.metrics_dict("Risk")

Walk-Forward Analysis

walk_forward() splits the simulation period into consecutive time-based folds and computes metrics for each fold independently. This helps assess strategy robustness across different market regimes.

See examples/walk_forward.ipynb

from alphavec import walk_forward, FoldConfig, MetricKey, tearsheet

result = walk_forward(
    weights=weights,
    market=MarketData(close_prices=close_prices, exec_prices=exec_prices),
    config=SimConfig(
        fee_rate=0.00025,
        slippage_rate=0.001,
        trim_warmup=True,  # Skip periods with no valid weights before folding
    ),
    fold_config=FoldConfig(
        fold_period="3ME",   # 3-month (quarterly) folds
        min_periods=30,      # Minimum observations per fold
        align_start=True,    # Align to calendar boundaries
    ),
)

# Aggregated metrics across folds
agg = result.metric_aggregation(MetricKey.ANNUALIZED_SHARPE)
print(f"Median Sharpe: {agg.median:.3f}")
print(f"Mean Sharpe: {agg.mean:.3f}")
print(f"Std: {agg.std:.3f}")

# Per-fold values
fold_sharpes = result.fold_metric_values(MetricKey.ANNUALIZED_SHARPE)

# Summary stats table
result.summary_stats([MetricKey.ANNUALIZED_SHARPE, MetricKey.MAX_DRAWDOWN_EQUITY_PCT])

# Per-fold details
for fold in result.folds:
    print(f"Fold {fold.fold_index}: {fold.start_period} to {fold.end_period}")
    print(f"  Sharpe: {fold.result.metric_value(MetricKey.ANNUALIZED_SHARPE):.2f}")

# Generate tearsheets
tearsheet(sim_result=result.full_result, output_path="full_tearsheet.html")
tearsheet(sim_result=result.folds[0].result, output_path="fold_0_tearsheet.html")

# Extract metrics (unified API)
result.metric_value(MetricKey.ANNUALIZED_SHARPE)  # From full_result
result.available_metrics("Performance")
result.metrics_dict("Risk")

Extracting Metrics (Unified API)

All result types (SimulationResult, GridSearchResults, WalkForwardResult) implement the MetricsAccessor protocol, providing a consistent interface for extracting metrics:

from alphavec import MetricKey, MetricsAccessor

# Works identically across all result types:
result.metric_value(MetricKey.ANNUALIZED_SHARPE)        # Single metric
result.metric_value(MetricKey.ALPHA, default=0.0)      # With default for missing
result.available_metrics()                              # All metric keys
result.available_metrics("Performance")                 # Filter by category
result.metrics_dict()                                   # All as dict
result.metrics_dict("Risk")                             # Filter by category

This unified API eliminates the need for nested access patterns:

# Before (verbose)
grid_results.best.result.metric_value(MetricKey.ANNUALIZED_SHARPE)
wf_result.full_result.metric_value(MetricKey.ANNUALIZED_SHARPE)

# After (concise)
grid_results.metric_value(MetricKey.ANNUALIZED_SHARPE)
wf_result.metric_value(MetricKey.ANNUALIZED_SHARPE)

Write generic functions that work with any result type:

def report_performance(result: MetricsAccessor) -> None:
    """Works with SimulationResult, GridSearchResults, or WalkForwardResult."""
    print(f"Sharpe: {result.metric_value(MetricKey.ANNUALIZED_SHARPE):.2f}")
    print(f"Max DD: {result.metric_value(MetricKey.MAX_DRAWDOWN_EQUITY_PCT):.2f}%")
    for key, value in result.metrics_dict("Performance").items():
        print(f"  {key}: {value}")

Note: For GridSearchResults, metrics delegate to best.result. For WalkForwardResult, metrics delegate to full_result (requires include_full_result=True). For fold-level metrics in walk-forward, use metric_aggregation() or fold_metric_values().

Metrics

simulate() returns a metrics DataFrame with Category, Value, and Note columns. Metrics are grouped into categories:

Categories

  1. Meta: Simulation metadata and configuration
  2. Performance: Returns, volatility, Sharpe ratio, drawdowns
  3. Costs: Fees, funding, turnover, order statistics
  4. Exposure: Gross/net leverage metrics
  5. Benchmark: Alpha, beta, tracking error, information ratio (CAPM)
  6. Distribution: Win/loss stats, skewness, kurtosis, drawdown duration
  7. Portfolio: Holding periods, weights, cost ratios
  8. Risk: Sortino, VaR, CVaR, Omega, downside deviation, Ulcer Index
  9. Signal: ICs, decile spreads, hit-rates, and selection vs directional decomposition (vs next-period returns)

Additional Diagnostics (for charting)

Some richer time series / grouped diagnostics are attached as metrics.attrs[...] for use in the tearsheet:

  • result.artifacts exposes typed access to the same artifacts (for autocomplete/discoverability)
  • metrics_artifacts(result.metrics) provides the same access when you only have the metrics table

Example:

art = result.artifacts
equity = art.equity
drawdown = art.drawdown_pct
rolling_sharpe = art.rolling_sharpe(30)
signal_ic = art.signal.weight_forward
  • metrics.attrs["returns"]: per-period returns (Series)
  • metrics.attrs["returns_pct"]: per-period returns in percent (Series)
  • metrics.attrs["equity"]: equity curve (Series)
  • metrics.attrs["equity_pct"]: equity curve as cumulative return percent (Series)
  • metrics.attrs["drawdown_pct"]: drawdown series in percent (Series)
  • metrics.attrs["rolling_sharpe_30"]: 30-period rolling Sharpe (annualized, Series)
  • metrics.attrs["init_cash"]: initial cash used for the simulation (float)
  • metrics.attrs["benchmark_equity"]: benchmark equity curve when benchmark_asset is provided (Series)
  • metrics.attrs["benchmark_equity_pct"]: benchmark equity as cumulative return percent (Series)
  • metrics.attrs["turnover"]: per-period turnover ratio (Series)
  • metrics.attrs["transaction_costs"]: per-period transaction costs as a fraction of equity (Series)
  • metrics.attrs["n_positions"]: per-period active positions (Series)
  • metrics.attrs["net_exposure"]: per-period net exposure ratio (Series)
  • metrics.attrs["gross_exposure"]: per-period gross exposure ratio (Series)
  • metrics.attrs["long_exposure"]: per-period long exposure ratio (Series)
  • metrics.attrs["short_exposure"]: per-period short exposure ratio (Series)
  • metrics.attrs["concentration"]: per-period position concentration (Herfindahl index, Series)
  • metrics.attrs["weight_forward"]: per-period signal diagnostics vs next returns (DataFrame), including:
    • ic, rank_ic
    • top_bottom_spread
    • selection vs directional attribution (and per-gross variants)
  • metrics.attrs["weight_forward_deciles"]: mean next return by weight decile (Series)
  • metrics.attrs["weight_forward_deciles_median"]: median next return by weight decile (Series)
  • metrics.attrs["weight_forward_deciles_std"]: std dev of next return by weight decile (Series)
  • metrics.attrs["weight_forward_deciles_count"]: observation count n by weight decile (Series)
  • metrics.attrs["weight_forward_deciles_contrib"]: mean contribution by weight decile (Series)
  • metrics.attrs["weight_forward_deciles_contrib_long"]: mean long contribution by weight decile (Series)
  • metrics.attrs["weight_forward_deciles_contrib_short"]: mean short contribution by weight decile (Series)
  • metrics.attrs["alpha_decay_next_return_by_type"]: alpha decay curve as a DataFrame indexed by lag (periods), with mean next return per gross and t-stats for:
    • total_per_gross_*, selection_per_gross_*, directional_per_gross_*

n is the number of (asset, timestamp) observations that fell into that decile with a non-zero weight and a finite next-period return.

Smoothing in the tearsheet is applied at render time only; the underlying per-period series remain available in metrics.attrs.

The rolling Sharpe series used in the tearsheet is stored as metrics.attrs["rolling_sharpe_N"] (N defaults to 30 when no smoothing is applied).

Statistical Methodology

Alphavec follows industry-standard statistical practices for backtesting:

  • Sample statistics (Bessel's correction, ddof=1) for all variance/standard deviation calculations
    • Rationale: Backtests are samples from possible market outcomes, not complete populations
    • Aligns with quantstats, empyrical, pyfolio, and academic finance literature
  • Geometric mean for total returns (compounds properly over time)
  • Arithmetic mean for active returns (matches tracking error calculation for Information Ratio)
  • Sample covariance for beta calculation (CAPM-consistent)
  • Excess kurtosis (normal distribution = 0, not 3)

This ensures alphavec metrics are directly comparable to industry benchmarks and professional analytics platforms.

Tearsheet

The built-in tearsheet() renderer produces a self-contained HTML report (static charts + metrics table), including:

  • Equity curve (portfolio and optional benchmark)
  • Drawdown
  • Rolling Sharpe (annualized, default 30 periods; uses smooth_periods when > 0)
  • Returns distribution
  • Signal diagnostics (when available): directionality and IC/rank-IC vs next-period return scatters with linear fits, alpha decay by lag (total/selection/directional), and decile charts (mean/median, long/short contribution)

Tearsheet Example

Tearsheet