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.
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.
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]"
- Simulates cross‑margin (cash pooling) with unlimited leverage and borrowing (no liquidations or margin calls).
- Orders execute at
exec_pricesplus 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_pricesorclose_pricesimply the asset is not tradable that period. - NaNs in
funding_ratesare treated as 0, and funding is always 0 whenclose_pricesis NaN. - Positions will always be closed if target weight is zero, regardless of minimum notional filter.
- Use
SimConfig.start_period/end_periodto slice inputs before simulating:strusesloc,intusesiloc.
simulate() runs a cross‑margin perpetual futures backtest from target portfolio weights.
Key inputs:
weights: pandasDataFramewith aDatetimeIndexand 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 asweights.
Returns:
returns: period returns as a pandasSeries.metrics: key performance metrics as a pandasDataFramewithValueandNotecolumns.
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,
)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() 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")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 categoryThis 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().
simulate() returns a metrics DataFrame with Category, Value, and Note columns. Metrics are grouped into categories:
- Meta: Simulation metadata and configuration
- Performance: Returns, volatility, Sharpe ratio, drawdowns
- Costs: Fees, funding, turnover, order statistics
- Exposure: Gross/net leverage metrics
- Benchmark: Alpha, beta, tracking error, information ratio (CAPM)
- Distribution: Win/loss stats, skewness, kurtosis, drawdown duration
- Portfolio: Holding periods, weights, cost ratios
- Risk: Sortino, VaR, CVaR, Omega, downside deviation, Ulcer Index
- Signal: ICs, decile spreads, hit-rates, and selection vs directional decomposition (vs next-period returns)
Some richer time series / grouped diagnostics are attached as metrics.attrs[...] for use in the tearsheet:
result.artifactsexposes 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_forwardmetrics.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 whenbenchmark_assetis 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_ictop_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 countnby 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).
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.
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_periodswhen > 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)
