← all projects
Case study · fintech data platform · verified AI

MarketIQ
verified numbers, not confident ones.

Most portfolio projects in this space predict prices. This one doesn't — it takes the position that in finance the hard problem is correctness: knowing what was true at a given moment, and refusing to report a number that can't be traced back to evidence. A bitemporal price store, a dataset-level quality gate, and an AI analyst whose every numeric claim is checked against the tool output its own prompt was built from.

Live demo ↗ Repository ↗
460
AUTOMATED TESTS
91.7%
GEMINI GROUNDING RATE
12/12
CLAIM VERIFICATION
9751%
FABRICATED MOVE CAUGHT
10/10
LOOKAHEAD EXPLOIT BLOCKED
MarketIQ portfolio command center — portfolio value, invested capital, return, unrealized P/L, allocation donut chart, and exposure by asset
The portfolio command center — value, allocation, and exposure by asset, computed entirely from prices already stored in PostgreSQL. No provider call.

01 · The problem

Most financial-data portfolio projects predict prices. This one takes the position that the harder, less glamorous problem in finance is correctness — knowing what was true at a given moment, proving a figure came from somewhere, and refusing to report one that didn't. Three properties follow, and each is measured rather than asserted: point-in-time correctness (prices are stored bitemporally — what was observed, and when it became known), verified answers (the AI analyst's numeric claims are checked against the tool output its prompt was built from), and honest measurement (every claim has a runnable demo that reports what it found, including when that was nothing).

A dataset-level quality gate independent of row validation runs before every publish. Row validation alone can't see a batch that's well-formed and wrong — a provider that truncates a year of history to three days still returns three perfect rows. The gate catches what row rules can't.

02 · Architecture

Yahoo Finance data lands through a daily Airflow-orchestrated ingestion, passes a dataset-level quality gate, and is stored bitemporally in PostgreSQL — every read requires an as-of. An AI analyst answers questions over that data, and every numeric claim it makes is checked against the tool output its prompt was built from before it reaches the user.

Yahoo Finance providerdaily market data · raw payload capture
INGEST → QUALITY GATE → CONDITIONAL PUBLISH
Bitemporal PostgreSQLobserved_at vs knowledge_date · as-of required on every read
GOLD LAYER · strictly-trailing windows, no lookahead
gold_daily_metricsreturns · volatility · moving averages
Point-in-time backtest enginefills at next-session open
AI ANALYST
Tool-calling chat (Gemini)selects only relevant portfolio/market tools
Claim verifiersupported / derived / contradicted / unsupported
SERVING
FastAPI backendlive on Render
Next.js frontendlive on Vercel
Airflow + Docker + GitHub Actionsdaily ingestion · CI on every push
MarketIQ holdings table showing asset, quantity, average price, current price, market value and return for six NSE-listed positions
Portfolio holdings — quantity, average price, market value and return, all Decimal-backed through provider parsing, normalization, persistence and serialization.

03 · The bug the pipeline caught in itself

On its first run against a real Airflow scheduler and real market data, the quality gate flagged an apparent 9751% single-day price move as implausible — an expectation designed for exactly this kind of dataset-level anomaly, not a row-level format check. 460 tests were green when it shipped. No exception, no failed test.

Tracing it by hand through raw SQL, by session-open time, found the root cause: a fix for an earlier missing-data problem had silently merged two different financial instruments into one database row — Infosys's USD-priced NYSE listing and its INR-priced NSE listing, same symbol, same day, two currencies stored as one price series. The bug was in my own fix for a prior issue, introduced hours earlier. It was root-caused and fixed with a regression test the same day.

04 · Results — reported honestly

Every row below runs offline from one command — no API key, no network, no database server. The point of each isn't a flattering number; several exist specifically to show where the system's own defenses were insufficient until proven otherwise.

RESULTCOMMAND
Survivorship biasHeadline conclusion changed once run across multiple seeds
Ingestion quality7 synthetic batches pass every row-level rule; 5 refused publication
Gold lookaheadTwo independent ways a derived table leaks the future
Backtest lookaheadEither protection alone blocks an exploit that lands 10/10 with both off ★
Live gate catchGate blocked a real ingestion failure on first live run; the fix introduced a worse bug — caught the same way
Claim verification12/12 agreement, 0 misses — after the harness found a bug in itself
Grounding evaluationA provider that states no figures scores a perfect grounding rate
Provider comparisonGemini scores 91.7% grounding at 1.7 claims/answer
Direction predictionTest AUC 0.5458 ± 0.1245 — ambiguous, not reported as an edge

The direction-prediction row is the one worth reading first. Grounding rate alone ranks a useless analyst joint-first with a correct one — it's never reported without claim density beside it, because a metric that silence can win isn't measuring honesty.

AI Analyst chat showing a portfolio performance answer with 2 of 4 claims grounded, 50 percent grounding rate, and 73 evidence facts, tagged ANNOTATED
The AI analyst, mid-verification — every numeric claim tagged against the evidence it was drawn from, not trusted on the model's word.
Stock Explorer showing RELIANCE.NS with real NSE price history, daily return, volatility, and average volume
Stock Explorer — real NSE price history and analytics computed server-side from stored, bitemporal data.

05 · Lessons learned

A control is worth nothing until its effect is measured.

The backtest's two lookahead protections were only "obviously" independent until an exploit was actually run 10/10 with both disabled — the number, not the intuition, is the finding.

Grounding a number in evidence isn't the same as the number being true.

Claim verification turns "don't invent figures" from a prompt instruction into an enforced postcondition — and a contradicted verdict, where the model read a labelled field and reported it wrong, is worth more than an unsupported one.

The fastest way to find a real bug is to run the system against reality.

460 green tests didn't catch the currency collision — a live scheduler against real market data did, on its first run, the same day the bug shipped.