Build a strategy
Learn the Algorithm contract: configuration, lifecycle hooks, data access, scheduling, orders, sizing, and risk controls.
An AlphaLens strategy is a Python class that subclasses Algorithm. The class declares the run configuration, receives completed bars, reads history, and submits target weights or orders. The same class can run as a backtest or as a local live deployment.
Strategy anatomy
Every strategy has three layers:
| Layer | What it contains |
|---|---|
| Configuration | start, end, universe, resolution, initial_cash, benchmark_symbol, sweep settings |
| Lifecycle | initialize, on_warmup_finished, on_data, scheduled methods, order events |
| Trading logic | history, history_array, history_arrays, set_holdings, orders, risk limits |
Here is the smallest useful shape:
from alphalens_core import Algorithm
class MyStrategy(Algorithm):
start = "2020-01-01"
end = "2025-12-31"
universe = ["SPY"]
resolution = "1day"
initial_cash = 100_000
benchmark_symbol = "SPY"
def initialize(self):
self.set_warmup(50)
def on_data(self, slice):
history = self.history("SPY", 50)
if len(history) < 50:
return
fast = history["close"].tail(10).mean()
slow = history["close"].mean()
self.set_holdings("SPY", 1.0 if fast > slow else 0.0)Run it with:
alphalens run --strategy my_strategy:MyStrategyConfiguration
Set defaults on the class so the CLI and Python API can run the strategy without a separate config file.
| Attribute | Purpose |
|---|---|
start / end | Backtest date window |
universe | List of symbols to subscribe to |
resolution | Bar size, such as 1day or 5minute |
initial_cash | Starting capital for backtests |
benchmark_symbol | Benchmark used for relative performance |
track_runs | Persist local run metadata |
cloud_enabled | Sync run data to AlphaLens when auth is configured |
progress | Emit low-frequency backtest progress logs |
sweep_grid | Parameter values for batch research |
Constructor parameters are useful for values you want to override with --param.
class Breakout(Algorithm):
start = "2021-01-01"
end = "2025-12-31"
universe = ["SPY", "QQQ", "IWM"]
resolution = "1day"
benchmark_symbol = "SPY"
lookback = 63
threshold = 0.05
sweep_grid = {
"lookback": [21, 63, 126],
"threshold": [0.03, 0.05, 0.08],
}Run one variant:
alphalens run --strategy my_strategy:Breakout --param lookback=126 --param threshold=0.03Lifecycle hooks
Use initialize for setup only. Configuration setters such as set_warmup, set_universe, and set_brokerage_model must be called there before the engine starts.
| Hook | When it runs |
|---|---|
initialize() | Once before data starts |
on_warmup_finished() | Once after warmup history is seeded |
on_data(slice) | On every completed bar |
on_order_event(event) | When an order updates or fills |
on_securities_changed(changes) | When the active universe changes |
on_end_of_day(symbol) | End-of-day event for a symbol |
For intraday strategies, keep on_data cheap. Let it ingest the bar and update state; use scheduled methods for heavier rebalances.
Warmup
Warmup gives the strategy enough history before the tradable window starts.
def initialize(self):
self.set_warmup(252)Use replay=False when your strategy only needs the retained history window and does not need every warmup bar replayed through on_data.
def initialize(self):
self.set_warmup(252, replay=False)This matters for large intraday universes because replaying every warmup bar can be expensive.
Data access
slice contains the current completed bar. history returns a pandas DataFrame for one symbol or a multi-index DataFrame for many symbols.
def on_data(self, slice):
if "SPY" not in slice:
return
close = self.history("SPY", 20)["close"]For faster cross-sectional strategies, use NumPy arrays:
timestamps, values, present = self.history_arrays(
["SPY", "QQQ", "IWM"],
126,
["close"],
)
closes = values[:, :, 0]history_arrays returns (timestamps, values, present) where values is shaped [periods, symbols, fields].
Scheduling
Scheduled methods keep signal generation and execution cadence explicit.
from alphalens_core import Algorithm, DateRules, TimeRules
class ScheduledStrategy(Algorithm):
resolution = "5minute"
def initialize(self):
self.set_warmup(390)
self.schedule.on(
DateRules.every_day(),
TimeRules.market_open(minutes_after=60),
self.rebalance,
)
def on_data(self, slice):
# Keep this lightweight for intraday runs.
self.last_slice = slice
def rebalance(self):
self.set_holdings("SPY", 1.0, tag="scheduled-rebalance")Use scheduling when you want 5-minute bars for state but only want to place orders at 10:00, 11:00, 12:00, and similar decision points.
Orders and target weights
Most strategies should use set_holdings. It computes the trade needed to reach a target portfolio weight.
self.set_holdings("SPY", 1.0, tag="risk-on")
self.set_holdings("TLT", 0.0, tag="risk-off")
self.set_holdings("SH", -0.25, tag="short-hedge")Use direct orders when you need order-type control.
| Method | Behavior |
|---|---|
market_order(symbol, quantity) | Fill at the next bar open |
market_on_open(symbol, quantity) | Explicit next-open order |
market_on_close(symbol, quantity) | Fill at the next bar close |
limit_order(symbol, quantity, limit_price) | Fill if the next bar crosses the limit |
stop_order(symbol, quantity, stop_price) | Trigger stop-market order |
stop_limit_order(symbol, quantity, stop_price, limit_price) | Trigger stop then require limit fill |
liquidate(symbol) | Close one symbol or all invested symbols |
set_holdings supports long, cash, and short targets. A target weight above 1.0 uses leverage when the brokerage model allows it.
Risk controls
Risk helpers are optional but useful for reusable guardrails.
def initialize(self):
self.set_warmup(252)
self.set_max_position_size("SPY", 0.50)
self.set_stop_loss("SPY", 0.08)set_max_position_size clamps future set_holdings calls. Manual order methods are not clamped.
You can also size by realized volatility:
qty = self.vol_targeted_quantity("SPY", target_annual_vol=0.15, lookback=63)
self.market_order("SPY", qty, tag="vol-target")Complete example
This example rotates into the strongest positive-momentum assets once per day.
import numpy as np
from alphalens_core import Algorithm, DateRules, TimeRules
class MomentumRotation(Algorithm):
start = "2018-01-01"
end = "2025-12-31"
universe = ["SPY", "QQQ", "IWM", "TLT", "GLD", "DBC"]
resolution = "1day"
initial_cash = 100_000
benchmark_symbol = "SPY"
lookback = 126
top_n = 2
max_weight = 0.50
sweep_grid = {
"lookback": [63, 126, 189],
"top_n": [1, 2, 3],
}
def initialize(self):
self.set_warmup(self.lookback + 1, replay=False)
self.schedule.on(
DateRules.every_day(),
TimeRules.market_open(minutes_after=1),
self.rebalance,
)
def rebalance(self):
symbols = [security.symbol for security in self.securities.values()]
if not symbols:
return
_, values, present = self.history_arrays(
symbols,
self.lookback + 1,
["close"],
)
closes = values[:, :, 0]
valid = present.all(axis=0) & np.isfinite(closes).all(axis=0)
momentum = closes[-1] / closes[0] - 1.0
scores = np.where(valid, momentum, -np.inf)
ranked = np.argsort(scores)[::-1]
winners = [
symbols[i].ticker
for i in ranked[: self.top_n]
if scores[i] > 0
]
winner_set = set(winners)
weight = min(1.0 / max(len(winners), 1), self.max_weight)
for symbol in symbols:
target = weight if symbol.ticker in winner_set else 0.0
self.set_holdings(symbol, target, tag="momentum-rotation")Run it locally:
ALPHALENS_API_KEY=alens_... alphalens run --strategy my_strategy:MomentumRotationThen open Strategy Center and select the synced backtest.
What to avoid
- Do not fetch future data inside strategy code.
- Do not put API keys or broker credentials on the strategy class.
- Do not do expensive model training inside every
on_datacall. - Do not rely on telemetry writes for trading correctness. Cloud sync is best-effort.
- Do not optimize only the headline return. Use drawdown, turnover, benchmark comparison, and out-of-sample checks.