Skip to content

Recipe: DuckDB and Polars

The same Parquet lake can be consumed in three ways: load() handles research queries with semantics, DuckDB handles cross-dataset SQL, and Polars scan() handles LazyFrame pipelines. Pick an entry point; do not make a copy as an "adjusted data lake".

Preparation

The examples below use the production configuration configs/cnequity.toml and require daily_bars and adj_factors to be published. To try it on a small lake, first run demo --research, and pass the demo configuration or data_root explicitly in each query.

DuckDB: cross-dataset aggregation

cne query --config configs/cnequity.toml --sql "
  SELECT symbol, max(trade_date) AS last_date, avg(adj_close) AS avg_hfq_close
  FROM daily_bars_adj
  WHERE trade_date >= DATE '2024-01-01'
    AND adj_is_exact
  GROUP BY symbol
  ORDER BY avg_hfq_close DESC
  LIMIT 20
"

daily_bars_adj is a read-only view with adj_* and adj_is_exact. cne query accepts only a single SELECT, which makes it suitable for handing SQL to scripts or an MCP agent.

Polars: LazyFrame feature pipeline

import polars as pl

from cnequity.query import load

bars = (
    load(
        "daily_bars",
        start="2024-01-01",
        end="2024-12-31",
        symbols=["600519.SH", "000001.SZ"],
        adjust="hfq",
        strict_adj=True,
    )
    .lazy()
    .sort(["symbol", "trade_date"])
    .with_columns(
        pl.col("adj_close").pct_change().over("symbol").alias("daily_return")
    )
)

features = bars.select(
    ["symbol", "trade_date", "adj_close", "daily_return"]
).collect()

load() first completes strict price adjustment and materializes the result; .lazy() only defers the feature computation that follows. Limit the input size by date and symbol; this is not lazy reading end to end.

If you only need raw market data, scan lazily directly:

from cnequity.query import scan

raw = scan("daily_bars", start="2024-01-01", end="2024-12-31",
           symbols=["600519.SH", "000001.SZ"])
print(raw.select("symbol", "trade_date", "close").limit(10).collect())

scan() does not accept adjust, strict_adj, universe or as_of, and does not perform the corresponding research checks for you.

Connecting to an AI agent

cne mcp --config /abs/path/to/cnequity.toml

The MCP server is read-only; its return values declare origin, truncation status, and the price adjustment and PIT semantics. Collection and maintenance are done by the cne CLI; the operations page of a local cne serve can preview and launch commands from the same allowlist. Other MCP clients can reuse the same command / args configuration; for the full tool contract, see the MCP reference.