Point-in-Time Correctness

Prerequisite: Auditability

Your fraud model’s backtest shows a 94% detection rate. You deploy it, and detection drops to 71% in production. The model is not wrong — the backtest is. During training, the model had access to features that were computed from data that arrived hours or days after the transaction it was scoring. The feature “number of chargebacks on this card in the last 30 days” included chargebacks that were filed after the transaction under evaluation. The model learned to use future information to classify past events, and now that future information is unavailable at inference time, it fails.

This is look-ahead bias. It is the most common and most expensive data error in financial systems, and it contaminates backtests, regulatory reports, and model training pipelines with equal ease.

The Two Clocks

Every fact in a financial system has two timestamps. The first is when the event happened in the real world — the trade executed, the payment cleared, the customer’s address changed. The second is when your system learned about it — the file arrived, the webhook fired, the batch job ingested the record.

These two clocks diverge constantly. A trade executes at 14:32:07 but the clearing record arrives at 22:15:00. A customer changes their address on March 3, but the KYC provider’s batch update lands on March 5. A chargeback is filed on April 12 against a transaction from March 1.

Event Real-world time System-knowledge time Gap
ACH return Original transaction date Return file arrival (T+2 to T+60) 2–60 days
Chargeback Original transaction date Dispute notification (T+1 to T+120) 1–120 days
KYC status change Customer action date Provider batch update 1–5 days
Sanctions list update OFAC publication time List ingestion completion 15 min–24 hours
Price correction Original trade time Exchange correction notice Minutes to days

A query that asks “what was true on March 15?” must specify which clock it means. “What had happened by March 15?” (real-world time) gives one answer. “What did we know on March 15?” (system-knowledge time) gives a different answer. Both are valid questions. Confusing them is the source of every point-in-time error.

See Auditability. The handbook’s three timestamps — value time, booking time, and settlement time — are specific instances of this two-clock problem. Point-in-time correctness generalizes the pattern to every data source in the system.

Look-Ahead Bias in Model Training

A fraud model trained on a dataset where each row contains the label “fraudulent” alongside features computed from the current state of the database has look-ahead bias by construction. The label was assigned weeks after the transaction. Features computed from the current database state include information that accumulated after the labeling event, which itself occurred after the transaction.

The correct approach: for each training example at time T, compute features using only data that was available at time T.

-- Wrong: current state of features
SELECT
  t.transaction_id,
  t.amount,
  (SELECT COUNT(*) FROM chargebacks c
   WHERE c.card_id = t.card_id
   AND c.filed_date < t.transaction_date + INTERVAL '30 days')
    AS chargebacks_30d,
  t.is_fraud
FROM transactions t;

-- Correct: features as known at transaction time
SELECT
  t.transaction_id,
  t.amount,
  (SELECT COUNT(*) FROM chargebacks c
   WHERE c.card_id = t.card_id
   AND c.filed_date < t.transaction_date
   AND c.ingested_at < t.transaction_date)
    AS chargebacks_30d,
  t.is_fraud
FROM transactions t;

The difference is the ingested_at filter. The first query counts chargebacks that existed by their filed date — but your system may not have known about them yet. The second query counts only chargebacks that your system had ingested before the transaction occurred. This is the information the model will have in production.

De Prado’s “Advances in Financial Machine Learning” identifies look-ahead bias as the primary cause of backtest overfitting in quantitative finance. The same principle applies to every ML model in fintech: fraud detection, credit scoring, AML alert prioritization, and churn prediction.

Look-Ahead Bias in Regulatory Reporting

A regulator asks: “On March 31, how many of your customers had completed KYC?” You query the current database and count customers where kyc_completed_date <= '2025-03-31'. The answer is 47,312.

The actual answer on March 31 was 46,891. The difference: 421 customers whose KYC was backdated. A support agent corrected a data entry error on April 5, changing a customer’s KYC completion date from April 2 to March 29. A batch import on April 10 added historical records that filled in missing KYC dates from March. Each correction is individually legitimate. Collectively, they make the current database answer a question about March 31 differently than the March 31 database would have.

For regulatory reporting, the question is almost always “what did you know, and when did you know it?” The current database cannot answer that question. Only a system that preserves the state of knowledge at each point in time can.

The Temporal Data Model

A point-in-time correct data store tracks both clocks for every record. The standard approach uses two timestamp columns on every row.

CREATE TABLE customer_kyc (
  customer_id    BIGINT NOT NULL,
  kyc_status     TEXT NOT NULL,
  valid_from     TIMESTAMPTZ NOT NULL,
  valid_to       TIMESTAMPTZ,
  recorded_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  superseded_at  TIMESTAMPTZ
);

valid_from and valid_to define when the fact was true in the real world. recorded_at and superseded_at define when the system knew about it. A correction does not update the existing row — it sets superseded_at on the old row and inserts a new row with the corrected values and a new recorded_at.

Query type Filter Returns
Current state valid_to IS NULL AND superseded_at IS NULL What is true now, as we currently understand it
As-of (real-world) valid_from <= T AND (valid_to IS NULL OR valid_to > T) AND superseded_at IS NULL What was true at time T, with all corrections applied
As-known (system) recorded_at <= T AND (superseded_at IS NULL OR superseded_at > T) What we knew at time T, without later corrections
As-of and as-known Both filters combined What we knew about time T₁ as of time T₂

The “as-known” query is the one that eliminates look-ahead bias. It returns exactly what the system knew at the specified time — no more, no less.

The “as-of” query with corrections applied is what most people mean when they say “point-in-time.” But for model training and regulatory “what did you know” questions, you need the “as-known” query — the state of knowledge, not the corrected state of reality. These are different questions with different answers.

Snapshot Isolation for Regulatory Queries

Running a regulatory report against a live database introduces a subtler form of temporal inconsistency. A report that takes 45 minutes to execute reads some tables at the beginning and others at the end. Transactions committed during those 45 minutes appear in late-read tables but not in early-read tables. The report is internally inconsistent — it reflects a state of the database that never existed at any single point in time.

Postgres REPEATABLE READ isolation solves this for single-database reports. The transaction sees a consistent snapshot from its start time, regardless of concurrent commits.

BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- All queries in this transaction see the database
-- as it existed at transaction start time
SELECT ... FROM accounts ...;
SELECT ... FROM transactions ...;  -- even if new txns committed
SELECT ... FROM balances ...;      -- sees same snapshot
COMMIT;

For multi-database reports — common in microservice architectures — there is no built-in snapshot isolation. Each database has its own transaction timeline. Two approaches work.

Coordinated snapshots. Before the report begins, each service records its current WAL position or transaction ID. Report queries include a filter: WHERE recorded_at <= snapshot_timestamp. All services agree on a single cutoff time, and the report reads only data recorded before that time.

Materialized snapshots. A scheduled job exports each service’s state to a data warehouse at a known time. The report runs against the warehouse. The snapshot is as fresh as the last export, but it is guaranteed internally consistent because it was captured atomically from each source.

Approach Consistency Freshness Complexity
Single-DB REPEATABLE READ Perfect Real-time Low
Coordinated snapshots Strong (clock-dependent) Real-time Medium
Materialized snapshots Perfect within snapshot Hours (batch cadence) Low

Most regulatory reports tolerate batch freshness. The quarterly call report does not need real-time data — it needs correct-as-of-quarter-end data. The materialized snapshot approach dominates for this reason.

Building the Point-in-Time Warehouse

A financial data warehouse that supports point-in-time queries needs three properties: every record carries both timestamps, no record is ever physically deleted or updated, and every query specifies which clock and which time it is asking about.

Ingestion. Every record entering the warehouse gets a warehouse_loaded_at timestamp assigned by the warehouse, not the source. Source timestamps (event_time, created_at) are preserved as-is but treated as claims about real-world time, not as facts about when the warehouse learned the information.

Storage. Append-only tables partitioned by warehouse_loaded_at. Partitioning by load time makes “as-known” queries efficient — “what did we know on March 31?” reads only partitions loaded on or before March 31. Corrections arrive as new rows in later partitions, not as updates to existing partitions.

Query patterns. Every analytical query and every model training pipeline must specify its temporal semantics. A query without a temporal filter is a bug. Enforce this with a query linter or a view layer that requires temporal parameters.

-- View that enforces temporal discipline
CREATE OR REPLACE FUNCTION customer_state(
  as_of TIMESTAMPTZ,
  as_known TIMESTAMPTZ DEFAULT now()
)
RETURNS TABLE (customer_id BIGINT, kyc_status TEXT, risk_tier TEXT)
AS $$
  SELECT DISTINCT ON (customer_id)
    customer_id, kyc_status, risk_tier
  FROM customer_kyc_history
  WHERE valid_from <= as_of
    AND (valid_to IS NULL OR valid_to > as_of)
    AND warehouse_loaded_at <= as_known
    AND (superseded_at IS NULL OR superseded_at > as_known)
  ORDER BY customer_id, warehouse_loaded_at DESC;
$$ LANGUAGE sql STABLE;

The function signature forces every caller to declare what time they are asking about. There is no default for as_of — the caller must choose. The as_known parameter defaults to now, which is correct for most analytical queries but must be overridden for model training and regulatory reporting.

Feature Stores and Point-in-Time Joins

The feature store is where point-in-time correctness is hardest to enforce and where violations are most damaging. A feature store that serves the fraud model must reconstruct the exact feature values that were available at the time of each historical transaction — not the values that are available now.

The point-in-time join is the critical operation. For each training example at time T, join to the feature store and retrieve the most recent feature value that was known before T.

-- Point-in-time join: latest feature value known before each transaction
SELECT
  t.transaction_id,
  t.transaction_time,
  f.value AS merchant_avg_ticket_30d
FROM transactions t
LEFT JOIN LATERAL (
  SELECT value
  FROM merchant_features f
  WHERE f.merchant_id = t.merchant_id
    AND f.feature_name = 'avg_ticket_30d'
    AND f.computed_at < t.transaction_time
  ORDER BY f.computed_at DESC
  LIMIT 1
) f ON true;

This query is expensive. For 10 million training examples and 50 features, it executes 500 million point-in-time lookups. Feast, Tecton, and other feature store platforms optimize this with pre-computed feature snapshots at regular intervals, reducing the lateral join to an interval lookup.

The operational cost of point-in-time correctness is real. Feature computation pipelines must track when each feature was computed, not just what value it produced. Feature backfills must be carefully versioned — a backfill that retroactively computes features for historical data creates look-ahead bias unless the pipeline uses only data available at each historical point.

A feature backfill that reads the current database and writes features timestamped in the past is the single most common source of look-ahead bias in financial ML systems. The backfill must replay the historical data state, not query the current state and backdate the result.

Late-Arriving Data

Financial data arrives late by design. ACH returns arrive 2–60 days after the original transaction. Chargebacks arrive 1–120 days later. Corrections arrive indefinitely. A point-in-time system must handle late arrivals without retroactively contaminating earlier snapshots.

The append-only model handles this naturally. A chargeback filed on April 12 against a March 1 transaction arrives as a new row with valid_from = March 1 (when the dispute references) and warehouse_loaded_at = April 12 (when the system learned about it). An “as-known” query for March 15 will not see this chargeback. An “as-known” query for April 15 will. Both answers are correct for their respective questions.

The challenge is reporting windows. A regulatory report for Q1 (January 1 – March 31) submitted on April 30 must decide: include late-arriving data that references Q1 events, or exclude it because it was not known during Q1? The answer depends on the specific regulatory requirement. Call reports use an “as-of” model (include corrections). Model validation uses an “as-known” model (exclude future information). Your warehouse must support both, and the report template must specify which one it needs.