Skip to main content

Qrucible: hybrid stateful backtesting engine with Rust hot path

Project description

Qrucible

CI Release PyPI License: Apache-2.0 Docs Production Ready

430x faster backtesting. Test your trading strategies in seconds, not minutes.

API Stability Guarantee

Qrucible v1.0.0+ follows Semantic Versioning. This means:

  • Patch releases (1.0.x): Bug fixes only. No breaking changes.
  • Minor releases (1.x.0): New features, backwards compatible. Your existing code will continue to work.
  • Major releases (x.0.0): Breaking changes. Migration guide will be provided.

The following public API is stable:

API Stability
run_backtest(data, config) Stable
run_backtest_with_signals(data, signals, config) Stable
grid_search(data, configs, metric, top_n) Stable
load_bars(path), load_bars_csv(path), load_bars_parquet(path) Stable
StrategyConfig and all parameters Stable
BacktestResult and all fields Stable
Order type configs (TrailingStopConfig, BreakEvenConfig, etc.) Stable
BarData class Stable

Internal APIs (names starting with _) may change without notice.

Quick Start (Beginners Start Here!)

3 lines of code. That's all you need.

pip install qrucible
import qrucible_easy as ez

# That's it! Runs a backtest with sample data
result = ez.backtest()
print(result)

Output:

  Backtest Results: Great!
  ────────────────────────────────────────
  Total Return:        12.34%
  Total Trades:            47
  Win Rate:             53.2%
  Sharpe Ratio:          1.42
  Max Drawdown:          8.21%

Use Your Own Data

# From a CSV file
result = ez.backtest("my_prices.csv")

# From a pandas DataFrame
result = ez.backtest(df)

# From a list of prices
result = ez.backtest([100, 102, 101, 105, 103, 108, 110])

Try Different Strategies

# Moving Average Crossover (default)
result = ez.backtest_ma(fast=10, slow=30)

# RSI - buy oversold, sell overbought
result = ez.backtest_rsi(period=14, oversold=30, overbought=70)

# MACD crossover
result = ez.backtest_macd()

# Bollinger Bands
result = ez.backtest_bollinger()

# Compare all strategies at once
results = ez.compare_strategies()

Interactive Demo

import qrucible_easy as ez
ez.demo()  # See Qrucible in action!

Or run the quickstart script:

python scripts/quickstart.py

That's the beginner guide! The rest of this README covers advanced features.


What is Qrucible?

Hybrid, stateful backtester with a Rust hot path and a clean Python API. Qrucible executes path-dependent logic (position sizing that reacts to wins/losses, stops, and exits) at vectorized speeds by keeping the critical loop in Rust and fanning out parameter grids with rayon. Docs: https://charlesfreidenreich.github.io/Qrucible/

430x faster than equivalent Python code. 1,000+ configs/second grid search throughput.

Key Features

Technical Indicators (20+)

Moving Averages:

  • SMA (Simple Moving Average)
  • EMA (Exponential Moving Average)
  • WMA (Weighted Moving Average)
  • DEMA (Double Exponential Moving Average)
  • TEMA (Triple Exponential Moving Average)
  • KAMA (Kaufman Adaptive Moving Average)
  • HMA (Hull Moving Average)

Momentum:

  • RSI (Relative Strength Index)
  • MACD (Moving Average Convergence Divergence)
  • Stochastic Oscillator (%K, %D)
  • Williams %R
  • MFI (Money Flow Index)
  • TSI (True Strength Index)
  • ROC (Rate of Change)
  • Ultimate Oscillator

Volatility:

  • Bollinger Bands
  • ATR (Average True Range)
  • Keltner Channels
  • Donchian Channels

Trend:

  • ADX (Average Directional Index)
  • CCI (Commodity Channel Index)
  • Aroon
  • Ichimoku Cloud
  • SuperTrend
  • Parabolic SAR

Volume:

  • OBV (On-Balance Volume)
  • VWAP (Volume Weighted Average Price)
  • A/D Line (Accumulation/Distribution)
  • CMF (Chaikin Money Flow)
  • Force Index

Advanced Order Types

  • Stop Loss: Fixed percentage or ATR-based
  • Take Profit: Fixed percentage targets
  • Trailing Stop: Percentage or ATR-based trailing stops with activation threshold
  • Break-Even Stop: Move stop to entry after reaching profit target
  • Time Stop: Exit after N bars or specified duration
  • Partial Exits: Scale out of positions at profit targets
  • Pyramiding: Scale into positions with configurable spacing and sizing

Risk Management

  • Risk-Per-Trade Sizing: Position sizing based on stop distance and risk budget
  • ATR-Based Stops: Dynamic stops based on volatility
  • Max Drawdown Exit: Close all positions when drawdown exceeds threshold
  • Reduce-After-Loss: Reduce position size after losing trades
  • Margin Tracking: Full margin accounting for short positions
  • Position Size Limits: Cap maximum position size as fraction of equity

Execution Realism

  • Commission: Fixed per-trade commission
  • Slippage: Basis points slippage model
  • Spread: Bid-ask spread modeling

Comprehensive Metrics

  • Returns: Total Return, Annualized Return, Annualized Volatility
  • Risk-Adjusted: Sharpe Ratio, Sortino Ratio, Calmar Ratio
  • Drawdown: Maximum Drawdown, Recovery Factor, Ulcer Index
  • Trade Statistics: Win Rate, Profit Factor, Payoff Ratio, Expectancy
  • Risk Metrics: VaR (95%), CVaR (95%), Kelly Criterion
  • Trade Analysis: MAE/MFE, Average Bars Held, Consecutive Wins/Losses

Performance

Implementation Configs Time (s) Throughput Speedup
Python loop 21 8.76 2.4 cfg/s 1.0x
Rust loop 21 0.02 1,035 cfg/s 431.9x

Tested on Apple Silicon (M-series) with 200,000 synthetic OHLCV bars. Results vary by hardware; run python scripts/benchmark.py to reproduce on your machine.

Install

pip install qrucible

For the beginner-friendly API with pandas support:

pip install qrucible[easy]

Prebuilt wheels are published for Linux, macOS, and Windows with Parquet and common compression codecs enabled (brotli, gzip, lz4, snappy, zstd). No Rust toolchain is required when installing from PyPI wheels.

Prerequisites (for local builds)

  • Python 3.10+
  • Rust toolchain

Setup (venv + deps)

python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt

Build and install (editable)

maturin develop --release

Cargo config forces PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 so builds succeed with CPython 3.13 until PyO3 adds official support.

One-command setup

make setup

Core Usage

import numpy as np
from qrucible import StrategyConfig, run_backtest, grid_search, load_bars

# bars: rows = time, cols = [ts_epoch_us, asset_id, open, high, low, close, volume]
ohlcv = np.random.lognormal(mean=0.0, sigma=0.02, size=(50_000, 5)).astype(np.float64)
ohlcv[:, 2] = np.minimum(ohlcv[:, 0], ohlcv[:, 3])
ohlcv[:, 1] = np.maximum(ohlcv[:, 0], ohlcv[:, 3])
timestamps = (np.arange(len(ohlcv), dtype=np.int64) * 1_000_000).astype(np.float64)
asset_ids = np.zeros(len(ohlcv), dtype=np.float64)
bars = np.column_stack([timestamps, asset_ids, ohlcv]).astype(np.float64)

config = StrategyConfig(
    strategy_type="MA_CROSS",
    fast_window=10,
    slow_window=30,
    ma_type="EMA",           # SMA, EMA, WMA, DEMA, TEMA, KAMA, HMA
    stop_loss=0.02,
    take_profit=0.04,
    risk_per_trade=0.01,
    initial_cash=1_000_000.0,
    reduce_after_loss=True,
    loss_size_factor=0.5,
)

result = run_backtest(bars, config)
print(result)

Strategy Types

# Moving Average Crossover
config = StrategyConfig(
    strategy_type="MA_CROSS",
    fast_window=10,
    slow_window=30,
    ma_type="EMA",
)

# RSI
config = StrategyConfig(
    strategy_type="RSI",
    rsi_period=14,
    rsi_upper=70.0,
    rsi_lower=30.0,
)

# MACD
config = StrategyConfig(
    strategy_type="MACD",
    macd_fast=12,
    macd_slow=26,
    macd_signal=9,
)

# Stochastic
config = StrategyConfig(
    strategy_type="STOCHASTIC",
    stoch_k_period=14,
    stoch_d_period=3,
    stoch_upper=80.0,
    stoch_lower=20.0,
)

# Bollinger Bands
config = StrategyConfig(
    strategy_type="BOLLINGER",
    bollinger_period=20,
    bollinger_std=2.0,
)

# ADX (Trend Following)
config = StrategyConfig(
    strategy_type="ADX",
    adx_period=14,
    adx_threshold=25.0,    # Only trade when trend is strong
)

# Ichimoku Cloud
config = StrategyConfig(
    strategy_type="ICHIMOKU",
    ichimoku_tenkan=9,
    ichimoku_kijun=26,
    ichimoku_senkou_b=52,
)

# SuperTrend
config = StrategyConfig(
    strategy_type="SUPERTREND",
    supertrend_period=10,
    supertrend_mult=3.0,
)

# VWAP
config = StrategyConfig(
    strategy_type="VWAP",
    vwap_std_mult=2.0,
)

# OBV (On-Balance Volume)
config = StrategyConfig(
    strategy_type="OBV",
    obv_ma_period=20,
)

Advanced Order Types

Trailing Stops

from qrucible import StrategyConfig, TrailingStopConfig

config = StrategyConfig(
    strategy_type="MA_CROSS",
    fast_window=10,
    slow_window=30,
    stop_loss=0.03,
    trailing_stop=TrailingStopConfig(
        enabled=True,
        trail_pct=0.02,           # Trail by 2%
        activation_pct=0.01,      # Activate after 1% profit
    ),
)

ATR-Based Trailing Stops

config = StrategyConfig(
    strategy_type="MA_CROSS",
    use_atr_stops=True,
    atr_period=14,
    atr_multiplier=2.0,
    trailing_stop=TrailingStopConfig(
        enabled=True,
        trail_atr_mult=2.0,       # Trail by 2x ATR
        activation_pct=0.02,
    ),
)

Break-Even Stops

from qrucible import BreakEvenConfig

config = StrategyConfig(
    strategy_type="RSI",
    rsi_period=14,
    stop_loss=0.02,
    break_even=BreakEvenConfig(
        enabled=True,
        trigger_pct=0.01,         # Move to break-even after 1% profit
        offset_pct=0.001,         # Lock in 0.1% profit
    ),
)

Time Stops

from qrucible import TimeStopConfig

config = StrategyConfig(
    strategy_type="MACD",
    stop_loss=0.02,
    time_stop=TimeStopConfig(
        enabled=True,
        max_bars=50,              # Exit after 50 bars max
    ),
)

Partial Exits (Scaling Out)

from qrucible import PartialExitConfig

config = StrategyConfig(
    strategy_type="MA_CROSS",
    stop_loss=0.02,
    take_profit=0.06,
    partial_exit=PartialExitConfig(
        enabled=True,
        exit_pct=0.5,             # Exit 50% of position
        trigger_pct=0.02,         # At 2% profit
        move_stop_to_entry=True,  # Move stop to break-even
    ),
)

Pyramiding (Scaling In)

from qrucible import PyramidConfig

config = StrategyConfig(
    strategy_type="ADX",
    adx_period=14,
    stop_loss=0.02,
    pyramid=PyramidConfig(
        enabled=True,
        max_entries=3,            # Maximum 3 entries
        entry_spacing_pct=0.01,   # Add every 1% in profit
        size_multiplier=0.5,      # Each add is 50% of initial size
    ),
)

Execution Realism

config = StrategyConfig(
    strategy_type="RSI",
    rsi_period=14,
    stop_loss=0.02,
    commission=10.0,        # $10 per trade
    slippage_bps=5.0,       # 5 basis points slippage
    spread_bps=10.0,        # 10 basis points spread
)

result = run_backtest(bars, config)
print(f"Total commission: ${result.total_commission:.2f}")
print(f"Total slippage: ${result.total_slippage:.2f}")
print(f"Total spread cost: ${result.total_spread_cost:.2f}")

Trade Ledger and Equity Curve

config = StrategyConfig(
    strategy_type="MA_CROSS",
    record_trades=True,
    record_equity_curve=True,
)
result = run_backtest(bars, config)

# Individual trades with MAE/MFE analysis
for trade in result.trade_ledger[:5]:
    print(f"{trade.side} {trade.qty:.0f} @ {trade.entry_price:.2f} -> {trade.exit_price:.2f}")
    print(f"  gross: ${trade.gross_pnl:.2f}, net: ${trade.net_pnl:.2f}")
    print(f"  bars held: {trade.bars_held}, exit reason: {trade.exit_reason}")
    print(f"  MAE: {trade.mae:.2%}, MFE: {trade.mfe:.2%}")

# Equity curve
for pt in result.equity_curve[-3:]:
    print(f"Equity: ${pt.equity:.2f}, Drawdown: {pt.drawdown:.2%}")

Comprehensive Results

result = run_backtest(bars, config)

# Returns
print(f"Total Return: {result.total_return:.2%}")
print(f"Annualized Return: {result.annualized_return:.2%}")
print(f"Annualized Volatility: {result.annualized_volatility:.2%}")

# Risk-Adjusted
print(f"Sharpe: {result.sharpe:.3f}")
print(f"Sortino: {result.sortino:.3f}")
print(f"Calmar: {result.calmar:.3f}")

# Drawdown
print(f"Max Drawdown: {result.max_drawdown:.2%}")
print(f"Recovery Factor: {result.recovery_factor:.2f}")
print(f"Ulcer Index: {result.ulcer_index:.2f}")

# Trade Statistics
print(f"Trades: {result.trades}")
print(f"Win Rate: {result.win_pct:.2%}")
print(f"Profit Factor: {result.profit_factor:.2f}")
print(f"Payoff Ratio: {result.payoff_ratio:.2f}")
print(f"Expectancy: ${result.expectancy:.2f}")
print(f"Avg Bars Held: {result.avg_bars_held:.1f}")

# Risk Metrics
print(f"VaR (95%): {result.var_95:.2%}")
print(f"CVaR (95%): {result.cvar_95:.2%}")
print(f"Kelly Criterion: {result.kelly_criterion:.2%}")

# Order Type Analysis
print(f"Stop Loss Exits: {result.stop_loss_exits}")
print(f"Take Profit Exits: {result.take_profit_exits}")
print(f"Trailing Stop Exits: {result.trailing_stop_exits}")
print(f"Signal Exits: {result.signal_exits}")
print(f"Partial Exits: {result.partial_exits}")
print(f"Pyramid Entries: {result.pyramid_entries}")

External Signal Mode

Bring your own signals (computed in Python/NumPy), and let Qrucible handle sizing, stops, take-profit, and metrics at Rust speed:

from qrucible import StrategyConfig, run_backtest_with_signals
import numpy as np

# Compute your own signals: 1=long, -1=short, 0=hold
close_prices = bars[:, 5]
sma = np.convolve(close_prices, np.ones(20)/20, mode='same')

signals = np.zeros(len(bars), dtype=np.int8)
signals[close_prices < sma] = 1
signals[close_prices > sma] = -1

config = StrategyConfig(
    strategy_type="EXTERNAL",
    stop_loss=0.02,
    take_profit=0.04,
)
result = run_backtest_with_signals(bars, signals, config)

Grid Search

grid = [
    StrategyConfig(
        strategy_type="MA_CROSS",
        fast_window=f,
        slow_window=s,
        ma_type=ma,
        stop_loss=0.02,
        take_profit=0.04,
    )
    for f in (5, 10, 15, 20)
    for s in (30, 50, 100, 200)
    for ma in ("SMA", "EMA", "WMA")
]

# Sort by Sharpe ratio, get top 10
top = grid_search(bars, grid, metric="sharpe", top_n=10)
for r in top:
    print(f"Sharpe: {r.sharpe:.3f}, Return: {r.total_return:.2%}")

Multi-Asset Support

# CSV or Parquet with schema: [ts_epoch_us, asset_id, open, high, low, close, volume]
bars = load_bars("data/multi_asset_bars.parquet")
result = run_backtest(bars, config)

Benchmark

python scripts/benchmark.py

Scale the benchmark:

QRUCIBLE_BENCH_MINUTES=200000 QRUCIBLE_BENCH_ASSETS=1 python scripts/benchmark.py

Real Data Demo

python scripts/demo_real_data.py

Docker

docker build -t qrucible:latest .
docker run --rm qrucible:latest

Dev image:

docker build -f Dockerfile.dev -t qrucible-dev:latest .
docker run --rm qrucible-dev:latest

Documentation

Contributing

See CONTRIBUTING.md for dev setup and workflow. Changelog entries live in CHANGELOG.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qrucible-1.0.0.tar.gz (2.5 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

qrucible-1.0.0-cp310-abi3-win_amd64.whl (2.1 MB view details)

Uploaded CPython 3.10+Windows x86-64

qrucible-1.0.0-cp310-abi3-manylinux_2_28_x86_64.whl (2.4 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ x86-64

qrucible-1.0.0-cp310-abi3-macosx_11_0_arm64.whl (2.2 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

File details

Details for the file qrucible-1.0.0.tar.gz.

File metadata

  • Download URL: qrucible-1.0.0.tar.gz
  • Upload date:
  • Size: 2.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for qrucible-1.0.0.tar.gz
Algorithm Hash digest
SHA256 40b6b76edf4bebcf44342b8d9aea8ba03a708d8c10842449ee00d6d7e11082aa
MD5 ee60a6cedb5320f03e30a8f6619ed589
BLAKE2b-256 94f75d00e557eaf0856a2dc68b0060f84bce63962cd8eb85688058184c8a1088

See more details on using hashes here.

Provenance

The following attestation bundles were made for qrucible-1.0.0.tar.gz:

Publisher: release.yml on itbcharles/Qrucible

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qrucible-1.0.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: qrucible-1.0.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 2.1 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for qrucible-1.0.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 1a45b7d64e38a951ee0518d63bf0a58e5998078d3b8993a01fb7a2f59eef4ca8
MD5 27b21f617a3426cd54b6ad589fed182e
BLAKE2b-256 bacecad6f5227f799ddc439ae0a095b2478dc985002ee4b9a5fbca7532abd9e5

See more details on using hashes here.

Provenance

The following attestation bundles were made for qrucible-1.0.0-cp310-abi3-win_amd64.whl:

Publisher: release.yml on itbcharles/Qrucible

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qrucible-1.0.0-cp310-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for qrucible-1.0.0-cp310-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 cbeda7901c0de67867894e85eade3ef99673c659022b070ad69bf74dfa6eff8e
MD5 7e0d102cd67c50f9e411454f87bfe829
BLAKE2b-256 76063a3f64252119df18e3b7467c06492641fd1ee9bb51d69292a0a855031d74

See more details on using hashes here.

Provenance

The following attestation bundles were made for qrucible-1.0.0-cp310-abi3-manylinux_2_28_x86_64.whl:

Publisher: release.yml on itbcharles/Qrucible

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file qrucible-1.0.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for qrucible-1.0.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e6a51f8e924701be48a2e190f42a6cb80770c201883ad4603b9b9213a9d6ce4a
MD5 a12435ac211fba93e1d41c485526d04a
BLAKE2b-256 7409d4bc0be3fb00b27df792aadb48f67dc1dde194a5ee16771933521b9f34c6

See more details on using hashes here.

Provenance

The following attestation bundles were made for qrucible-1.0.0-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on itbcharles/Qrucible

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page