MLQ.ai
About Sign in Subscribe
API GUIDES · COOKBOOK

Point-in-time fundamentals for backtesting

Query the numbers as they were known on any date — no look-ahead bias, provable from the source filing.

The problem: your backtest knows the future

Most fundamentals APIs serve current truth: the latest filed value for every period, restatements silently applied. That's correct for research today — and quietly fatal for backtests. Two ways history leaks in:

  • Filing lag. A December fiscal year isn't knowable on January 1 — the 10-K typically arrives in late February. A backtest that reads FY numbers "in January" trades on data that didn't exist.
  • Restatements. When a company restates, ordinary APIs overwrite the original. Your backtest then sees the corrected number on dates when the market was trading on the wrong one.

Every financial endpoint here accepts as_of=YYYY-MM-DD: the response contains only values filed on or before that date. Facts are stored append-only, keyed by SEC accession number, so "what was known when" is a query — not a reconstruction.

1 · See it work

Apple's FY2025 10-K was filed on 2025-10-31. Ask for the latest annual income statement as of October 1, 2025 — before that filing existed:

curl -H "Authorization: Bearer $MLQ_KEY" \
  "https://mlq.ai/api/v1/companies/AAPL/income-statement/?period=FY&limit=1&as_of=2025-10-01"

You get FY2024 — the most recent annual data any investor actually had on that date. Drop the as_of and the same call returns FY2025. Every period carries a source block; check source.filed and you'll see why each value is or isn't visible at your cutoff. That's the audit trail: the constraint isn't our promise, it's the filing date on the record.

2 · Build a point-in-time factor table

The core loop of any fundamentals backtest: for each rebalance date, fetch metrics as of that date. Here's a quality screen (ROIC + margins) over quarterly rebalances:

import requests, pandas as pd

BASE = "https://mlq.ai/api/v1"
H = {"Authorization": "Bearer YOUR_KEY"}

UNIVERSE = ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN", "META",
            "AVGO", "ORCL", "COST", "LLY"]          # yours: /companies/?q=
REBALANCES = ["2025-03-01", "2025-06-01", "2025-09-01", "2025-12-01"]

rows = []
for date in REBALANCES:
    for t in UNIVERSE:
        r = requests.get(f"{BASE}/companies/{t}/metrics/",
                         params={"period": "FY", "as_of": date}, headers=H)
        if r.status_code != 200:
            continue                                 # not covered / no data yet
        j = r.json()
        m = j.get("metrics") or {}
        rows.append({
            "date": date, "ticker": t,
            "fy": j.get("fiscal_year"),              # the FY visible on that date
            "roic": m.get("return_on_invested_capital"),
            "gross_margin": m.get("gross_margin"),
            "net_margin": m.get("net_margin"),
            "accession": (j.get("source") or {}).get("accession"),  # receipts
        })

df = pd.DataFrame(rows)
# rank within each rebalance date — top-half ROIC, positive margins
df["roic_rank"] = df.groupby("date")["roic"].rank(pct=True)
picks = df[(df.roic_rank > 0.5) & (df.net_margin > 0)]
print(picks.sort_values(["date", "roic"], ascending=[True, False]))

Two details doing quiet work: fy tells you which fiscal year was visible at each date (it changes mid-year as 10-Ks land — that's the point), and accession gives every input a receipt you can open on EDGAR.

3 · Statement-level history, same rule

Need raw line items instead of ratios? Same parameter on the statement endpoints — and period=quarter returns the quarterly series (all quarters, newest first):

r = requests.get(f"{BASE}/companies/NVDA/income-statement/",
                 params={"period": "quarter", "limit": 8, "as_of": "2025-06-01"},
                 headers=H)
for p in r.json():
    print(p["date"], p["period"], p["revenue"], p["source"]["accession"])

For maximum granularity, /companies/{ticker}/facts/?as_of= returns every normalized concept with its filing provenance — the raw material if you compute your own factors.

4 · Pitfalls worth knowing

  • fiscal_year = the calendar year the period ends in. NVDA's January-2026 year-end is fiscal_year: 2026. Align on date (period end), not labels.
  • Q4 cash-flow values are derived (annual minus nine-month, marked form: "derived") — companies don't file a separate Q4 report.
  • Nulls are deliberate. Price-dependent metrics (P/E, market cap) are null — there's no price feed, so nothing here can silently mix stale prices into your factors. If a company doesn't tag a figure, you get null, never an imputed guess.
  • Definitions are documented, not smoothed. Operating income is as-filed (restructuring included); debt excludes operating leases; bank revenue is the standard net basis. Field-by-field notes live in the data dictionary.
  • Pace yourself. Free tier is 100 requests/day — fine for prototyping the loop above; the paid tiers (100k/1M per month) fit full-universe runs. List endpoints paginate with limit + offset.

Using an agent instead?

Everything above works over the MCP server — the statement, metrics, and facts tools all take as_of. Ask Claude: "Using MLQ, what were MSFT's margins as known on 2025-03-01, and cite the filing."

Fuel-Cell Deployments at U.S. Data CentersReport
Research

Fuel-Cell Deployments at U.S. Data Centers

Public disclosures confirm at least 154 MW of operating fuel cells at U.S. data centers. Seven proposed projects account for another 6.31 GW, led by large developments in New Mexico, Texas, and Wyoming.

No spam. Unsubscribe anytime.