Strategies¶
The contract¶
Every strategy implements BaseStrategy.generate_signals(data, features) ->
pd.DataFrame:
- Index: dates.
- Columns: symbols.
- Values: in
[-1, 1]—+1max long,0flat,-1max short.
Strategies produce signals, not portfolio weights, costs, or returns. Their
output must have exactly the same dates and symbols as the adjusted-close
matrix. Missing warm-up signals become zero; infinite values, duplicate axes,
misaligned axes, and finite values outside [-1, 1] are rejected.
Built-in strategies¶
Name (config strategy.name) |
Class | Idea |
|---|---|---|
buy_and_hold |
BuyAndHoldStrategy |
Constant long signal for every asset with a valid price; the allocator and rebalance cadence determine the actual portfolio. |
time_series_momentum |
TimeSeriesMomentumStrategy |
Per-asset trailing momentum; binary, continuous, or volatility-adjusted scaling. |
cross_sectional_momentum |
CrossSectionalMomentumStrategy |
Ranks assets by trailing momentum, longs the top fraction, and can short a disjoint bottom fraction. |
mean_reversion |
MeanReversionStrategy |
A stateful entry/exit/stop machine driven by a chosen indicator (rolling z-score, Bollinger %B, RSI, distance to moving average, or percentile rank). |
trend_following |
TrendFollowingStrategy |
Trailing MA-crossover direction; sizing belongs to the portfolio allocator. |
pairs_trading |
PairsTradingStrategy |
Trailing regression residual, ADF-gated entries, and legs sized by the relative dollar-notional hedge ratio implied by beta and current prices. |
Signals and allocators¶
An allocator decides how signal values become portfolio weights:
equal_weightpreserves signs but discards relative signal magnitudes.signal_proportionalpreserves relative signed magnitudes.inverse_volatilityandvolatility_targetingadd their own risk sizing.
Consequently, non-binary time-series or cross-sectional momentum signals
require an allocator that preserves signal magnitudes and cannot use
equal_weight; time-series momentum's volatility_adjusted mode
additionally cannot be combined with another inverse-volatility allocator.
Pairs trading requires signal_proportional; per-leg
weight/minimum-size constraints that would distort or remove one hedge leg are
rejected by configuration validation. A portfolio-level volatility target is
still valid because it scales both pair legs together.
The pairs regression estimates a share hedge ratio from the preceding
formation_window. QuantLab converts it to relative dollar notionals using
the current prices. The ADF test is run periodically on the complete trailing
formation-window residual and an inconclusive test prevents a new entry.
Stop-loss / take-profit¶
Every strategy except buy_and_hold accepts optional stop_loss_pct/
take_profit_pct parameters (fractional, None by default). Unlike
mean_reversion's stop_threshold (an indicator-level stop) or
pairs_trading's own stop_threshold (a spread-indicator-level stop),
these operate on
the position actually executed by the allocator/constraints/rebalancing/
execution pipeline, not on the strategy's own signal or indicator — see
Backtesting: Stop-loss / take-profit
for the exact mechanism, including how pairs_trading's two legs are
force-flattened together on their combined P&L via
BaseStrategy.position_groups().
Adding a new strategy¶
from collections.abc import Mapping
from typing import Any
import pandas as pd
from quantlab.strategies.base import BaseStrategy, register_strategy
@register_strategy("my_strategy")
class MyStrategy(BaseStrategy):
def __init__(self, lookback_period: int = 20) -> None:
values = self.validate_parameters({"lookback_period": lookback_period})
self.lookback_period = values["lookback_period"]
self._freeze_parameters()
@classmethod
def validate_parameters(cls, parameters: Mapping[str, Any]) -> dict[str, Any]:
values = dict(parameters)
if isinstance(values["lookback_period"], bool) or not isinstance(
values["lookback_period"], int
):
raise ValueError("lookback_period must be an integer")
if values["lookback_period"] <= 0:
raise ValueError("lookback_period must be positive")
return values
def generate_signals(
self, data: pd.DataFrame, features: pd.DataFrame | None = None
) -> pd.DataFrame:
prices = self._prices(data)
signal = ... # trailing-only strategy logic
return self._validate_signals(signal, prices)
Register it by importing the module once, then reference its name in
strategy.name. validate_parameters() lets configuration loading validate
relations without constructing the strategy. Constructors should still
validate direct Python use, and _freeze_parameters() prevents later public
parameter mutation.
To also give it a dashboard Strategy Explorer page (research content, an interactive lab, optionally its own Results-tab/report diagnostics), see that document's "Adding Strategy Explorer support for a new strategy" section — a separate, optional step from registering the strategy itself.
Look-ahead safety¶
Every built-in strategy uses trailing windows only. forward_returns exists
for research labels and must never feed a trading signal. Accounting shifts
held weights by one bar before applying asset returns (see
Backtesting); this prevents same-bar signal execution, but it
cannot make an incorrectly future-aware custom strategy causal. Custom
strategies remain responsible for never reading data after the signal date.