Backtest every signal as of the second you traded.
Not six hours later.
Join daily bars on their date and a 09:30 rebalance quietly reads the 16:00 close. On the same synthetic universe, that one-day leak turned a momentum strategy that made 11.3% a year into one that claimed 24.9%, past a benchmark it actually trailed. Chalk computes every windowed signal as of each rebalance, in one offline query.
Same strategy, same prices, two backtests
Pick a strategy and tune it. The solid curve trades on signals Chalk computed as of each 09:30 rebalance; the dashed one reads them from a table joined on the trading date, so it has already seen that day's close. Costs apply to every trade. The recorded run shows immediately; re-run it live on Chalk.
Hold the 8 tickers with the highest total return over the lookback. Long only, equal weight, from 40 synthetic tickers.
Checking the live backend…
Growth of $1, after costs
Recorded run · 2026-10-15| Weekly · 3 months · 5 bps | Total | CAGR | Sharpe | Max DD | Turnover/yr | Costs |
|---|---|---|---|---|---|---|
| Point in time | 46% | 11.3% | 0.63 | −41% | 26.1× | 4.6% |
| Leaky (date join) | 119% | 24.9% | 1.21 | −34% | 27.5× | 4.8% |
| Benchmark | 64% | 15.0% | 0.85 | −34% | – | – |
Reading each day's close before trading made momentum look better by 13.6 percentage points a year and moved its Sharpe from 0.63 to 1.21. The leaky run beats the benchmark; the real one does not.
The leaky signal already knows how the day it trades on will close, so it buys what is about to rise. The more often you rebalance, the more those few hours are worth: try daily.
The 40 tickers and their prices are synthetic, generated from a fixed seed with planted trend, choppy and sell-off regimes. Nothing here is market data or investment advice. The features, offline queries and portfolio math are real and run on Chalk.
What one rebalance saw
TKR01 at the 09:30 rebalance on Tuesday, 5 March 2024. Both rows are real output from the demo's offline queries; only the input time differs.
How it works
Look-ahead bias is a property of how signals are computed, not of the strategy. Put the time logic in the feature definitions once and every backtest inherits it.
Stamp bars when they print
Each daily bar carries the moment its close printed, 16:00 New York, not the trading date it belongs to. A 09:30 decision cannot see it.
Signals filter on chalk_now
Momentum, rolling volatility and the mean-reversion z-score are windowed aggregations over bars with ts <= chalk_now. The same definitions serve live trading.
One offline query per backtest
Every ticker at every rebalance is one input row, with its input time set to the decision. The recorded run reads 36,520 rows in under 30 seconds.
Trade it with costs
A Chalk scaling group turns signals into weights, trades at the open, charges basis points on turnover and reports the equity curve, Sharpe and drawdown.
Backtest your own signals
Define bars with the time each one became known, write signals as windowed aggregations that filter on chalk_now, and run one offline query with an input time per rebalance.
To use your own data, swap the synthetic resolver for a SQL resolver over your bars table. The signals, the query and the portfolio code do not change.
import chalk.functions as F
from chalk import DataFrame, Windowed, windowed
from chalk.features import FeatureTime, _, features, has_many
@features
class Bar:
id: str
ticker_id: str
close: float
ret: float # log return, previous close to this one
ts: FeatureTime # when the close printed, not the trading date
@features
class Ticker:
id: str
bars: DataFrame[Bar] = has_many(lambda: Bar.ticker_id == Ticker.id)
# Only closes that had printed by the query's "now".
last_close: float = F.max_by(_.bars[_.close, _.ts <= _.chalk_now], _.ts)
momentum: Windowed[float] = windowed(
"30d", "91d", "182d",
expression=_.bars[_.ret, _.ts > _.chalk_window, _.ts <= _.chalk_now].sum(),
default=0.0,
)
volatility: Windowed[float] = windowed(
"30d", "91d", "182d",
expression=_.bars[_.ret, _.ts > _.chalk_window, _.ts <= _.chalk_now].std(),
)
mean_close: Windowed[float] = windowed(
"30d", "91d", "182d",
expression=_.bars[_.close, _.ts > _.chalk_window, _.ts <= _.chalk_now].mean(),
)
std_close: Windowed[float] = windowed(
"30d", "91d", "182d",
expression=_.bars[_.close, _.ts > _.chalk_window, _.ts <= _.chalk_now].std(),
)
zscore_30d: float = (_.last_close - _.mean_close["30d"]) / _.std_close["30d"]What Chalk handles
Time travel for every signal, the compute to trade on it, and a record of each run.
One definition, research to production
The signal you backtest is the feature your live strategy queries. No second pipeline to rebuild the windows, and no drift between the two.
Windows as of any moment
30, 91 and 182-day windows end at each row's input time, to the second, so a rebalance only ever sees closes that had printed.
Checkable, row by row
Output the time of the last close each signal read next to its input time. In the recorded run, zero point-in-time rows read the future.
Backtests are datasets
Each offline query is saved as a named dataset with its own id, so a backtest can be re-read, compared and audited later.
Compute next to the data
The portfolio step runs in a Chalk scaling group that authenticates with its workload identity. No client secret in the container.
Numbers you can trade
The leaky backtest of the default strategy claimed 24.9% a year. Point in time, it made 11.3%, behind the benchmark's 15.0%.
Trust the Sharpe your backtest reports
Signals on bars, quotes, filings or alternative data, at any frequency. Talk to an engineer about computing them point in time, for research and production alike.