Sixty Institutions Now Ask LSEG's Data Questions Through MCP. Here Is The Text-To-SQL Layer That Makes 'What Was Our Exposure On 12 March?' Safe To Answer
LSEG's real-time feed passes 15 million data points a second from nearly 600 venues, its tick history spans 100 million instruments over 30 years and fields more than five million customer requests a month - and more than 60 financial institutions now reach it through MCP servers built with Anthropic, OpenAI, Microsoft, Databricks and Snowflake. Every one of those integrations ends in the same request: a person asks a question in English and something has to turn it into a query over columnar market data. Text-to-SQL is the most demanded and most dangerous pattern in financial AI, because a plausible query that ignores the as-of date, joins on today's constituents or scans thirty years of ticks is wrong in ways nobody sees. This is the layer we build between the model and the database, with the schema contract, point-in-time rewriting, cost bounds and validation that make it deployable.
AlchmAI Engineering14 min read
60+
Financial institutions connected to LSEG's MCP servers, built with Anthropic, OpenAI, Microsoft, Databricks and Snowflake
15m/sec
Data points on LSEG's real-time feed from nearly 600 exchanges and venues - the scale a careless query scans
100m x 30y
Instruments and years in LSEG's proprietary tick history, fielding 5m+ customer requests a month
2 dates
Every financial fact has a period and a knowledge time. Text-to-SQL that handles one is text-to-look-ahead-bias
There is a demo every financial data vendor and every bank's innovation team has now built: type a question, get a chart. The demand behind it is real and the numbers say so. LSEG - whose real-time feed exceeds 15 million data points a second from nearly 600 venues, whose tick history spans 100 million instruments over 30 years, and which fields over five million tick-history requests a month - has partnered with Anthropic, OpenAI, Microsoft, Databricks, Snowflake and Rogo on MCP-based access, and more than 60 financial institutions have connected. Its chief executive's framing is that more data consumption drives more insight, which drives more trading and more demand for risk management. The pattern that makes the demo work is text-to-SQL: a model turns the question into a query, the query runs, the result comes back.
The pattern that makes the demo dangerous is the same one. A model is very good at producing a query that runs and returns plausible numbers. It has no way of knowing that the question 'what was our exposure to the bank sector on 12 March' must join to the sector classification as it stood on 12 March, must use positions as known on 12 March rather than as later corrected, and must not be answered by scanning thirty years of ticks because the user typed 'exposure' rather than 'price'. We build the layer between the model and the warehouse for exactly this reason, and it has four parts.
Part One: The Model Sees A Semantic Contract, Not The Schema
The first decision is what the model is allowed to know. Handing it the raw warehouse schema - two hundred tables, each with a knowledge_time column it may or may not notice - is how you get the wrong-time failure. Instead we publish a semantic contract: a small set of views with business names, explicit grain, declared time semantics and the aggregations that are valid on each measure. The model writes SQL against the contract. Nothing else is reachable.
# The ONLY surface the model may query. Every view is bitemporal-aware and
# every measure declares how it may be aggregated. Anything not here is
# invisible, not merely discouraged.
views:
positions:
grain: [as_of_date, book, instrument_id]
time_semantics: point_in_time # requires as_of; rewriter enforces it
dimensions: [as_of_date, book, desk, instrument_id, sector, currency]
measures:
quantity: { agg: [sum], unit: units }
market_value: { agg: [sum], unit: base_ccy }
weight_pct: { agg: [none], unit: percent, note: "ratio - never sum or average" }
entitlement: desk # rows filtered by the caller's desk entitlement
instrument_classification:
grain: [instrument_id, valid_from, valid_to, known_from, known_until]
time_semantics: bitemporal # joins must be as-of on BOTH axes
dimensions: [instrument_id, sector, industry, index_membership]
daily_prices:
grain: [instrument_id, trade_date]
time_semantics: point_in_time
dimensions: [instrument_id, trade_date, venue]
measures:
close: { agg: [none, min, max, last], unit: price, note: "never sum" }
volume: { agg: [sum], unit: units }
adjusted_close: { agg: [none, min, max, last], unit: price, note: "split/dividend adjusted; say which" }
cost_hint: { rows_per_instrument_year: 252 }
tick_history:
grain: [instrument_id, ts_ns]
time_semantics: point_in_time
cost_hint: { rows_per_instrument_day: 2_000_000 }
require_explicit_window: true # rewriter refuses unbounded scansPart Two: The Rewriter Makes The Query Point-In-Time Whether The Model Did Or Not
The model is asked to produce SQL against the contract with an as-of date. It will sometimes forget, sometimes join a bitemporal view on only one axis, and sometimes put the date on the wrong side of a join. Rather than hoping, we parse the query and rewrite it: every point-in-time view gets its as-of predicate injected, every bitemporal join gets both axes, and the entitlement filter is added from the caller's identity rather than trusted from the SQL. The model's job is the shape of the question. The rewriter's job is the correctness of the time.
import sqlglot
from sqlglot import exp
PIT_VIEWS = {"positions": "as_of_date", "daily_prices": "trade_date", "tick_history": "ts_ns"}
BITEMPORAL = {"instrument_classification": ("valid_from", "valid_to", "known_from", "known_until")}
def rewrite(sql: str, as_of: str, entitlement: dict, contract: dict) -> str:
"""Parse, then enforce. Never regex a query; never trust the model's WHERE."""
tree = sqlglot.parse_one(sql, read="duckdb")
# 0. Only contract views may appear. Anything else is a hard refusal.
for t in tree.find_all(exp.Table):
if t.name not in contract["views"]:
raise Refused("TABLE_NOT_IN_CONTRACT:" + t.name)
# 1. Inject the as-of predicate on every point-in-time view, regardless of
# what the model wrote. The user's question decides as_of; the model does not.
for t in tree.find_all(exp.Table):
if t.name in PIT_VIEWS:
col = exp.column(PIT_VIEWS[t.name], table=t.alias_or_name)
pred = exp.LTE(this=col, expression=exp.Literal.string(as_of)) if t.name != "positions" else exp.EQ(this=col, expression=exp.Literal.string(as_of))
tree = _and_where(tree, pred)
# 2. Bitemporal joins must be as-of on BOTH axes: valid at the date, and known
# by the date. A join on validity alone is how June's restatement answers March.
for t in tree.find_all(exp.Table):
if t.name in BITEMPORAL:
vf, vt, kf, ku = BITEMPORAL[t.name]; a = t.alias_or_name
tree = _and_where(tree, exp.and_(
exp.LTE(this=exp.column(vf, a), expression=exp.Literal.string(as_of)),
exp.GT(this=exp.column(vt, a), expression=exp.Literal.string(as_of)),
exp.LTE(this=exp.column(kf, a), expression=exp.Literal.string(as_of)),
exp.or_(exp.Is(this=exp.column(ku, a), expression=exp.Null()),
exp.GT(this=exp.column(ku, a), expression=exp.Literal.string(as_of)))))
# 3. Entitlement comes from the caller, injected here, never from the model.
if "positions" in {t.name for t in tree.find_all(exp.Table)}:
tree = _and_where(tree, exp.In(this=exp.column("desk", "positions"),
expressions=[exp.Literal.string(d) for d in entitlement["desks"]]))
return tree.sql(dialect="duckdb")Part Three: Validation That Understands Money
After rewriting, the query is validated against the contract's measure rules. This is where the wrong-arithmetic failures are caught, and it is cheap because the contract already says what each measure permits.
- 01Aggregation legality. SUM over a price, AVG over a ratio, any aggregate over a measure declared agg: [none]. Each is a refusal with a code the model can read and correct.
- 02Adjustment declaration. A query touching daily_prices must select either close or adjusted_close explicitly and the response must say which. A chart across a split with the wrong one is a visible cliff; a table is a silent error.
- 03Currency consistency. A SUM of market_value across books in different base currencies is refused unless a conversion view is joined. The contract declares units precisely so this is checkable.
- 04Grain respect. Aggregating positions without grouping by as_of_date across a date range double-counts. If the query spans dates, the group-by must include the date or the measure must be a last-value.
- 05Result-shape declaration. The model declares whether it expects a scalar, a series or a table. A scalar question that produces four thousand rows is a wrong query, and cheaper to refuse than to render.
Part Four: Cost Bounds Before Execution
The tick store is where careless queries go to run for an hour. The contract's cost hints let the guard estimate rows before execution and refuse or narrow rather than discover the problem in a warehouse bill.
import duckdb
def bound_cost(sql: str, contract: dict, con: duckdb.DuckDBPyConnection, max_rows_scanned: int = 50_000_000) -> None:
"""Refuse before running. EXPLAIN is cheap; a thirty-year tick scan is not."""
plan = con.execute("EXPLAIN (FORMAT JSON) " + sql).fetchone()[0]
est = _estimated_rows_scanned(plan) # walk the plan's scan nodes
# tick_history without an explicit window is refused outright, whatever the estimate;
# the contract marks it require_explicit_window and the rewriter checks the predicate exists.
if est > max_rows_scanned:
raise Refused("COST_BOUND_EXCEEDED:est=" + str(est) +
" hint=narrow the date window or the instrument list")
# Belt and braces at execution: a hard statement timeout and a memory cap.
con.execute("SET statement_timeout = '20s'")
con.execute("SET memory_limit = '4GB'")The refusal messages are written for the model, not for a log. A refusal that says which bound was exceeded and how to narrow the query is a refusal the model can act on in one retry; a bare error sends it looking for a creative alternative, which in a query layer means a different and possibly worse query.
The Whole Path, And What Gets Logged
- 01Extract the as-of date, the scope and the intended result shape from the question as a structured step, and confirm them back to the user when ambiguous. 'Exposure on 12 March' is unambiguous; 'recent exposure' is not.
- 02Generate SQL against the semantic contract only, with the contract's measure rules in context so the model has a chance to be right first time.
- 03Parse and rewrite: as-of on every point-in-time view, both axes on every bitemporal join, entitlement from identity.
- 04Validate against the contract's arithmetic and unit rules; refuse with actionable codes.
- 05Bound the cost from the plan; refuse or narrow; execute under a timeout and memory cap.
- 06Return the rows with the as-of date, the adjustment basis and the entitlement scope stated alongside - the answer is a claim about a date and a scope, and it should say so.
- 07Log the question, the extracted as-of, the generated SQL, the rewritten SQL, the refusals, the estimated and actual rows, the caller and the model snapshot. When someone disputes a number in six months, the question is always 'what did it run', and this is the only cheap way to answer.
Why This Is The RAG Problem In Different Clothes
A reader of our hybrid-retrieval and context-engineering playbooks will recognise every part of this. The semantic contract is the chunking decision; the rewriter is the mandatory as-of filter; validation is the reranker that understands the domain; cost bounds are the token budget. Text-to-SQL is structured retrieval, and it fails in the same places unstructured retrieval does - time, scope, arithmetic - with the added property that its failures come back as tidy tables that look like facts. The discipline transfers directly, and so does the payoff: sixty-plus institutions on LSEG's MCP servers are going to ask their data questions in English from now on, and the firms whose answers are right about the date will be the ones with a layer like this between the model and the warehouse.
The Bottom Line
Natural-language access to market data has stopped being a demo - LSEG's 15 million points a second, 100 million instruments over 30 years and 60-plus institutions on MCP say so - and text-to-SQL is the pattern underneath it. It is also the most dangerous pattern in financial AI, because a model produces queries that run and return plausible numbers while ignoring the as-of date, joining today's world onto yesterday's question, aggregating a ratio, or scanning the tick store. The layer that makes it safe has four parts: a semantic contract that is the only surface the model can see, a rewriter that stamps the user's as-of date on every time-varying view and both axes of every bitemporal join with entitlement from identity, validation that knows which measures may be summed and which adjustment was used, and cost bounds from the plan before a query runs. Return every answer with its date and scope stated, log everything that was run, and the question 'what was our exposure on 12 March' gets the answer that was true on 12 March. That is the LLM engineering we do for financial data in London, and it is the difference between a chat box over a warehouse and a system a desk can trust.
References & Further Reading
- Markets Media - LSEG is 'more valuable in an AI world' (MCP partnerships, data scale, 60+ institutions connected). marketsmedia.com/lseg-more-valuable-in-an-ai-world
- FinTech Global - How LSEG and Microsoft are reshaping finance with AI. fintech.global/2026/03/31/how-lseg-and-microsoft-are-reshaping-finance-with-ai
- Amazon Web Services - Cloud adoption update for financial market infrastructure providers, 1H26 (LSEG Real-Time Optimized). aws.amazon.com/blogs/industries/financial-market-infrastructure-providers-cloud-adoption-update-for-1h26
- sqlglot - SQL parser, transpiler and optimizer (Python). github.com/tobymao/sqlglot
- DuckDB - EXPLAIN and query profiling. duckdb.org/docs/stable/guides/meta/explain.html
- DuckDB - ASOF joins. duckdb.org/docs/stable/sql/query_syntax/from.html
- Snodgrass - Developing Time-Oriented Database Applications in SQL (bitemporal foundations). www2.cs.arizona.edu/~rts/tdbbook.pdf
- Anthropic - Effective context engineering for AI agents. anthropic.com/engineering/effective-context-engineering-for-ai-agents
AlchmAI Engineering
Engineering, London
Written by the AlchmAI engineering team in Mayfair, London. We build trading platforms, real-time charts, market data pipelines and AI features for brokers, prop firms and fintech teams. The Playbook is where we explain how we approach these systems, with code you can run and sources you can check.
Code in this guide is illustrative and supplied without warranty. Review and test it before production use. Nothing here is investment advice. Important information