https://github.com/breaded-xyz/alphavec
Fast minimalist backtesting for perpetual futures.
https://github.com/breaded-xyz/alphavec
algorithmic-trading backtesting crypto crypto-bot quant quantitative-finance systematic-trading-strategies
Last synced: 5 months ago
JSON representation
Fast minimalist backtesting for perpetual futures.
- Host: GitHub
- URL: https://github.com/breaded-xyz/alphavec
- Owner: breaded-xyz
- License: mit
- Created: 2025-12-12T08:09:06.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2026-01-10T20:52:03.000Z (5 months ago)
- Last Synced: 2026-01-11T06:12:43.869Z (5 months ago)
- Topics: algorithmic-trading, backtesting, crypto, crypto-bot, quant, quantitative-finance, systematic-trading-strategies
- Language: Python
- Homepage: https://breaded.xyz
- Size: 46.5 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Alphavec
[](https://github.com/breaded-xyz/alphavec/actions/workflows/ci.yml)
[](https://github.com/breaded-xyz/alphavec/actions/workflows/python-publish.yml)
>**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...
```python
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`
```python
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`
```python
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`
```python
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:
```python
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:
```python
# 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:
```python
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:
```python
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
