Define an executable decision contract, build point-in-time features and completed labels, and validate the complete forecasting and trading process with chronological folds. Preserve the out-of-sample forecasts, translate them into orders with a fixed policy, and simulate execution and costs. Use one untouched holdout to evaluate the final specification.
Build the pipeline around a decision clock
- Specify the universe, decision clock, execution clock, label horizon and baseline. For a daily model, generate a forecast after close and enter at open . Target the return from that open to open .
- Construct trailing features and point-in-time joins. In each fold, train only with labels complete by the fit cutoff. Fit preprocessors and any feature selection within the training fold.
- Score forward validation blocks. Choose a model and a rule for sizing, rebalancing and risk using only those blocks. Write down the selection rule before evaluating the final holdout.
- Backtest predictions through an execution layer. Compare forecast error or rank correlation with turnover, net compounded return, drawdown and results at higher costs. Preserve the order and fill trace.
Keep one ledger that ties the research row to a realisable trade. Give the feature snapshot, model version, forecast, order and fill a shared decision identifier. You should be able to trace a surprising P&L row back to the exact data the model consumed. Then test the most fragile handoffs. Delaying a release should affect only later decisions. Rebuilding a historical dataset should produce the same features. Charging a fee should change net cash at the fill without changing the model forecast.
Assemble a point-in-time research loop
This example assumes sorted daily data in daily.csv with open and close columns and consecutive valid sessions. It uses a preset threshold and fold schedule for illustration.
import numpy as np
import pandas as pd
from sklearn.linear_model import Ridge
from sklearn.model_selection import TimeSeriesSplit
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
bars = pd.read_csv("daily.csv")
close = bars["close"]
data = pd.DataFrame({
"momentum": close.pct_change(10),
"volatility": close.pct_change().rolling(10).std(),
"target": bars["open"].shift(-2) / bars["open"].shift(-1) - 1,
}).dropna()
features = ["momentum", "volatility"]
predictions = []
for train, test in TimeSeriesSplit(n_splits=5, gap=2).split(data):
model = make_pipeline(StandardScaler(), Ridge(alpha=10.0))
model.fit(data[features].iloc[train], data["target"].iloc[train])
predictions.append(pd.Series(model.predict(data[features].iloc[test]),
index=data.index[test]))
forecast = pd.concat(predictions)
weight = (forecast > 0.001).astype(float)
turnover = weight.diff().abs().fillna(weight)
net_growth = (1 - 0.0005 * turnover) * (1 + weight * data.loc[weight.index, "target"])
wealth = np.prod(net_growth) * (1 - 0.0005 * weight.iloc[-1])
print("Walk-forward return after liquidation:", wealth - 1)