Build a backtest with point-in-time data
A backtest is only as honest as its inputs. The common failure is look-ahead: using a quarterly result on the day the quarter ended, weeks before the company published it. This guide builds a small daily panel of prices and trailing earnings for one stock where every value was public on its date.
1. Pull the full price history
Section titled “1. Pull the full price history”Price history pages backwards with end_date. Each call serves up to 2,000 calendar days.
import os, requests, pandas as pd
BASE = "https://api.tapetide.com/v1"s = requests.Session()s.headers["Authorization"] = f"Bearer {os.environ['TAPETIDE_TOKEN']}"
def price_history(symbol): rows, end = [], None while True: params = {"days": 2000, **({"end_date": end} if end else {})} body = s.get(f"{BASE}/stocks/{symbol}/price", params=params, timeout=60).json() rows.extend(body["data"]) win = body["meta"]["window"] if not win["has_more"]: break end = win["next_cursor"] df = pd.DataFrame(rows).drop_duplicates("date") df["date"] = pd.to_datetime(df["date"]) return df.sort_values("date").set_index("date")
px = price_history("TCS")close is adjusted for splits and bonuses, so returns computed from it are continuous across corporate actions. It is not dividend-adjusted: add dividends separately if you need total return. raw_close is the unadjusted print, null where unavailable.
2. Pull fundamentals with their availability dates
Section titled “2. Pull fundamentals with their availability dates”fin = s.get(f"{BASE}/stocks/TCS/financials", params={"section": "quarterly"}, timeout=60).json()q = fin["data"][0] if isinstance(fin["data"], list) else fin["data"]
eps = pd.Series(q["data"]["EPS in Rs"]) # {"Jun 2026": 36.9, ...}avail = {a["period"]: a["available_from"] for a in q["availability"]}
eps_pit = pd.DataFrame({ "eps": eps, "available_from": pd.to_datetime(pd.Series(avail)),}).dropna().sort_values("available_from")Each period’s available_from is the result-declaration date when basis is reported, or a regulatory-lag estimate when it is estimated. Treat estimated rows with care, or drop them, in sensitive tests.
3. Join on the date the data became public
Section titled “3. Join on the date the data became public”eps_pit["ttm_eps"] = eps_pit["eps"].rolling(4).sum()panel = pd.merge_asof( px[["close"]].reset_index(), eps_pit[["available_from", "ttm_eps"]].rename(columns={"available_from": "date"}), on="date", direction="backward", # only rows already published on each date)panel["pe"] = panel["close"] / panel["ttm_eps"]merge_asof(..., direction="backward") gives each trading day the latest quarter that had been published by then, never one from the future.
4. Respect the trading calendar
Section titled “4. Respect the trading calendar”Use the holiday list rather than a weekday calendar. Exchanges sometimes open on weekends for special sessions and close on weekdays.
holidays, url = [], "/v1/market/holidays?from=2015-01-01&limit=500"while url: body = s.get("https://api.tapetide.com" + url, timeout=30).json() holidays += [h["date"] for h in body["data"]] url = body["links"]["next"]A trading date with no bar is not a zero return. Mask it, do not fill it with 0.
Caveats to state in your results
Section titled “Caveats to state in your results”- Statement values are the latest reported figures and can include restatements made after
available_from. - Today’s index constituents are not yesterday’s. Building a universe from a current index list adds survivorship bias.
- Prices are end-of-day. Intraday bars are not served by these routes.
Each call above costs one request unit. A full price history is a handful of calls; cache it locally rather than refetching per run.