Live on Chalk · Point-in-time backtests

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.

Strategy

Hold the 8 tickers with the highest total return over the lookback. Long only, equal weight, from 40 synthetic tickers.

Lookback
Rebalance

Checking the live backend…

Growth of $1, after costs

Recorded run · 2026-10-15
$1$1.5$2202320242025
2022-07-04 → 2025-12-31Point in time$1.46Leaky (date join)$2.19Equal-weight benchmark$1.64
Weekly · 3 months · 5 bpsTotalCAGRSharpeMax DDTurnover/yrCosts
Point in time46%11.3%0.63−41%26.1×4.6%
Leaky (date join)119%24.9%1.21−34%27.5×4.8%
Benchmark64%15.0%0.85−34%––
What the leak did

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.

Point in time · dataset c60987c3 · 36,520 rows in 28.6s · 0 rows read a close that had not printed
Date join · dataset af579e84 · 36,520 rows in 27.3s · 36,520 rows read a close that had not printed

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.

Point in time
Mon 4 Mar, 16:00
$50.52
17.5 hours before the decision. Every window ends here.
Date join
Tue 5 Mar, 16:00
$49.75
Six and a half hours after the trade. The signal already knows the day fell 1.5%.

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.

Getting started

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.