Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QuantLib

A modular backtesting framework for equities with built-in machine learning support and strict data validation. Strategies — from a moving-average crossover to XGBoost / Random Forest factor models — are composed from small, independently testable components and executed by a single engine.

QuantLib is a library, not an application. Your factors, models, selectors, and strategies live in your own code; QuantLib only provides the registry, the engine, and the evaluation tools.

Pipeline

Each backtest run consists of five stages:

Stage Responsibility
DataFeed Validates required columns, prints available fields, and splits the raw frame into per-symbol Stock entities
Factors @ql.factor computes indicators; ML models score via factor functions (quantlib.utils.check_fields validates input fields)
Selectors @ql.selector filters and truncates the daily universe before the strategy runs
Strategies @ql.strategy executes buy/sell logic on the surviving stocks
Analyzer Plots NAV curves, drawdowns, and trade markers for side-by-side comparison

Design Principle: Fail Fast

Missing data must never be silently backfilled: a 0 in the wrong column can turn bad data into a catastrophic trade. QuantLib therefore validates inputs at three layers and aborts immediately on violation.

Layer Validation On failure
DataFeed Required columns Date, Stock, Open, Close present ValueError at construction; missing columns printed
Model inference check_fields reports missing / extra columns against training features Missing -> ValueError; extra -> warning and ignored
Strategy required_fields declared and checked per strategy KeyError, execution blocked

Installation

From the project root:

pip install -e .
pip install xgboost  # optional; XGBoost is skipped when unavailable

Usage

1. Prepare data

QuantLib consumes a pandas.DataFrame (typically read from CSV or Parquet):

import pandas as pd
df = pd.read_parquet("data/ml_merged.parquet")  # must contain Date, Stock, Open, Close

DataFeed prints all available columns on load so you can verify the schema before the run.

2. Define your own factors

Register indicators with @ql.factor. Decorator arguments are forwarded to the function, so one implementation can back several factors:

# my_components.py
from quantlib import ql

@ql.factor("MidPrice")
def mid_price(stock_df):
    return (stock_df["Open"] + stock_df["Close"]) / 2.0

@ql.factor("MA_5", column="MidPrice", window=5)
@ql.factor("MA_20", column="MidPrice", window=20)
def moving_average(stock_df, column="Close", window=5):
    return stock_df[column].rolling(window=window).mean()

3. Train a model (outside the framework)

Training is intentionally not part of QuantLib. Tree models, CNNs and Transformers each need their own Dataset / loss / epoch loop, and no single template can hold them. Train wherever you like (Jupyter, a standalone script) with sklearn, xgboost, PyTorch, etc., then persist the estimator together with its feature list so the framework can validate inference columns:

# your_script.py -- train with sklearn directly, save in the loadable format
import joblib
from sklearn.ensemble import RandomForestRegressor

rf = RandomForestRegressor(n_estimators=200, max_depth=10, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
joblib.dump({'model': rf, 'features': FEATURE_COLS}, 'models/rf_v1.joblib')

examples/train_model.py trains both RandomForest and XGBoost this way and saves the better one as models/best_model.joblib.

3b. Load a model inside a factor

Models are not a framework concept: predicting is just what a factor function does. You load your own model (sklearn, xgboost, PyTorch, Transformer — anything with a predict), and the framework provides one optional helper for the repetitive part: field validation.

check_fields is a generic field checker — it tells you which required fields are missing and which extra fields are present, for any DataFrame / Series / dict:

from quantlib.utils import check_fields

result = check_fields(stock_df, feats)        # missing -> ValueError (default); extra -> warning
print(result.missing, result.extra, result.ok)  # FieldCheck(missing=[...], extra=[...])
X = stock_df[feats].fillna(0)                 # pick columns + fill NaN yourself
scores = model.predict(X)                     # Booster needs xgb.DMatrix(X)

Example — an XGBoost factor with user-side loading:

@ql.factor("AI_Score", model_file="xgb_alpha.ubj", meta_file="xgb_alpha.joblib")
def ai_score(stock_df, model_file, meta_file):
    meta = joblib.load(f"models/{meta_file}")          # {'features': [...]}
    model = xgb.Booster()
    model.load_model(f"models/{model_file}")
    check_fields(stock_df, meta["features"])           # missing / extra field report
    X = stock_df[meta["features"]].fillna(0)
    return pd.Series(model.predict(xgb.DMatrix(X)), index=stock_df.index)

4. Score stocks with a model

A generic sklearn-style factor (works with any estimator that has a predict — joblib-pickled {'model': ..., 'features': [...]}):

@ql.factor("AI_Score", model_file="best_model.joblib")
def ai_score(stock_df, model_file="best_model.joblib"):
    meta = joblib.load(f"models/{model_file}")          # {'model': est, 'features': [...]}
    model, feats = meta["model"], meta["features"]
    check_fields(stock_df, feats)                       # missing / extra field report
    X = stock_df[feats].fillna(0)
    return pd.Series(model.predict(X), index=stock_df.index)

5. Select a universe

@ql.selector("Top_3_Momentum")
def top_3_momentum(date, snapshot):
    ranked = sorted(snapshot.items(), key=lambda x: x[1].get("MA_5", 0), reverse=True)
    return dict(ranked[:3])  # only the top names reach the strategy

6. Write your own strategy

Every strategy declares the fields it depends on and validates them before trading; a missing field aborts execution.

@ql.strategy("AI_Score_Strategy")
def ai_strategy(date, snapshot, portfolio, close_prices):
    required_fields = ["Close", "AI_Score"]
    for stock in portfolio.stocks:
        if stock not in snapshot:
            continue
        data = snapshot[stock]
        missing = [f for f in required_fields if f not in data]
        if missing:
            raise KeyError(f"{stock} missing required fields: {missing}")
        if data["AI_Score"] > 0.0002:
            portfolio.order_target_percent(date, stock, 0.5, data["Close"], close_prices)
        elif data["AI_Score"] < -0.0001:
            portfolio.order_target_percent(date, stock, 0.0, data["Close"], close_prices)

7. Run a backtest

# backtest.py
from quantlib import ql, DataFeed, Portfolio, Engine, Analyzer
import my_components  # registers your factors / selectors / strategies

feed = DataFeed(df_raw)
port = Portfolio(initial_cash=1_000_000, fee=0.0001, stocks=stocks)
engine = Engine(data_feed=feed, ql_context=ql)

history = engine.run(
    strategy_name="AI_Score_Strategy",
    factors=["MidPrice", "MA_5", "MA_20", "MOM_10", "VOL_5", "AI_Score"],
    portfolio=port,
    start="2025-08-01",
    end="2026-07-31",
    selector_name="Top_3_Momentum",  # optional
)

results = {"AI_Score_Strategy": {"history": history, "trades": port.trades}}
Analyzer.plot_multi_strategies(results, initial_cash=1_000_000)

8. Generate today's orders (live)

Decider is the live counterpart of Engine: it runs a registered strategy once for today against a real Portfolio (real positions / cash / stock pool) and returns the orders the strategy tried to place, instead of executing them on an exchange. Route the returned list to your broker.

from quantlib import ql, Portfolio, Decider
import my_components  # registers your strategies

# real account state
port = Portfolio(initial_cash=1_000_000, fee=0.0001, stocks=stocks)
decider = Decider(ql_context=ql)

# today_snapshot: {stock: {'Close': ..., 'AI_Score': ...}} from your live data feed
orders = decider.generate_today_orders(
    strategy_name="AI_Score_Strategy",
    portfolio=port,
    today_snapshot=today_snapshot,
    today_date=today_date,
    required_fields=["Close", "AI_Score"],  # optional: fail-fast snapshot validation
)
# orders -> broker

Alpha Example

examples/alpha_components.py demonstrates a complete ML-driven setup that reached +76% cumulative return on the 2025-08-01 ~ 2026-07-31 window (equal-weight market benchmark -1.3%, max drawdown -22%):

  1. examples/fe_engine.py — builds 14 cross-sectional z-scored features (momentum / volatility / turnover / size / distance-to-extremes) plus a 10-day forward-return label from ml_merged.parquet.
  2. examples/train_alpha.py — trains an XGBoost booster with the native xgb.train engine (early stopping on a time-split validation set) and saves it as models/xgb_alpha.ubj (+ a {'features': [...]} metadata joblib); the AI_Score factor loads it itself.
  3. examples/alpha_components.py — registers AI_Score (factor), Top_10_Score (selector), and AI_TopN_Weekly (strategy: rebalance every 7 calendar days, top-10 equal weight, clear the rest).
  4. examples/run_backtest_alpha.py — runs the full backtest through Engine and plots the equity curve.

The strategy rebalances into the top-10 stocks by model score, holding them equally weighted.

For a full step-by-step reproduction (data requirements, feature engineering, training, backtest, and live Decider usage), see examples/ALPHA_GUIDE.md. A short file-by-file index lives in examples/README.md.

Data note: data/ and models/ are git-ignored (large artifacts). After cloning, prepare your own daily quote parquet (see examples/ALPHA_GUIDE.md for the required columns) and run the feature / training / backtest scripts.

Live Decider Example

examples/decider_example.py shows the Decider in action:

python examples/decider_example.py          # offline demo using historical data
python examples/decider_example.py --live   # try the East Money real-time crawler

Project Layout

quantlib/
├── pyproject.toml           # packaging; pip install -e .
├── quantlib/                # the library
│   ├── __init__.py          # exports ql, DataFeed, Stock, Portfolio, Engine, Decider, Analyzer, FieldCheck, check_fields
│   ├── context.py           # global registry + decorators
│   ├── core/
│   │   ├── data_feed.py     # schema validation, field printing, per-stock split
│   │   ├── stock.py         # single-stock entity + live snapshot (East Money crawler)
│   │   ├── portfolio.py     # cash, positions, costs, drawdown tracking
│   │   ├── engine.py        # backtest daily loop: factors -> selection -> strategy -> settlement
│   │   └── decider.py       # live one-shot decision: runs a strategy once, returns today's orders
│   └── utils/
│       ├── analyzer.py      # NAV / drawdown / trade-marker plots
│       └── fields.py        # check_fields / FieldCheck (generic field validator)
├── examples/                # user-side code (index: examples/README.md)
│   ├── basic/               # framework tour: components / training / backtest
│   ├── alpha/               # ML cross-sectional pipeline + ALPHA_GUIDE.md
│   └── live/                # Decider example + East Money diagnostics
├── data/                    # input parquet (git-ignored)
├── models/                  # persisted models (git-ignored)
└── .gitignore               # excludes data/, models/, caches

See examples/ for a complete, runnable setup.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages