Per-Symbol Minute VWAP in DuckDB
Data Engineering for Quants · Snippet ID: duckdb-asof-vwap
What it does. Minute VWAP is the traded value in that minute divided by the traded volume, computed separately for each symbol. DuckDB's time_bucket assigns each timestamp to the start of its minute, and GROUP BY ALL groups by the selected symbol and minute columns. Division uses NULLIF so a minute whose volume sums to zero gets a missing VWAP instead of an error or an infinity. The path to the Parquet files is passed as a parameter.
Approach
minute = time_bucket(INTERVAL 1 MINUTE, timestamp) VWAP = Σ(price · volume) / Σ volume within (symbol, minute) volume = Σ volume SELECT symbol, minute, VWAP, volume FROM read_parquet(?) -- path is bound, not concatenated GROUP BY ALL ORDER BY symbol, minute
When to use it
- You want minute bars from a large tape and you do not want to materialise every tick as a DataFrame. The aggregation runs in DuckDB and only the bars come back.
- The same VWAP, computed after loading the ticks, is pandas-tick-to-ohlcv. Session VWAP as an execution benchmark is the VWAP term.
- Attaching quotes to trades is a different operation. That as-of join is polars-asof-join-quotes. This query does not look at quotes.
Pitfalls
- The slug says asof. The query is a group-by on a time bucket. There is no ASOF JOIN in it. Keeping the slug stable matters more than renaming the page to match the SQL keyword.
- Timestamp ties and order. VWAP does not care which trade came first. Open and close would. Do not extend this query with an unordered
first(price)and call it the open. - Zero volume.
NULLIFmakes VWAP null when the minute's volume is 0. The row is still returned, so a caller can see the empty minute. Filter it if you want only real bars. - Clock and time zone.
time_bucketuses the timestamp as stored. Naive and timezone-aware columns must not be mixed across files.
Python reference implementation
Requires duckdb. Tested before publication; the generator re-executes it on every rebuild. MIT licensed. Download duckdb-asof-vwap.py · All snippets
import duckdb
def minute_vwap(parquet_glob: str):
"""Minute VWAP and volume per symbol from Parquet trades.
parquet_glob is a bound parameter (a file path or a glob). The query is an
aggregation on time_bucket, not an as-of join.
"""
if not isinstance(parquet_glob, str) or parquet_glob == "":
raise ValueError("parquet_glob must be a non-empty string")
con = duckdb.connect()
return con.execute(
"""
SELECT symbol,
time_bucket(INTERVAL 1 MINUTE, timestamp) AS minute,
SUM(price * volume) / NULLIF(SUM(volume), 0) AS vwap,
SUM(volume) AS volume
FROM read_parquet(?)
GROUP BY ALL
ORDER BY symbol, minute
""",
[parquet_glob],
).df()
FAQ
Why is this not an as-of join?
VWAP needs every trade in the minute, weighted by size. An as-of join would keep a single neighbouring row. The page id was fixed by the batch that added it; the SQL matches the definition above, and the as-of join lives on the Polars quotes page.
Why bind the path?
The glob is data. Concatenating it into the query string turns a peculiar folder name into SQL syntax. read_parquet(?) keeps it a parameter, including globs.
Summary for AI agents (Markdown mirror)
# Per-Symbol Minute VWAP in DuckDB
> Aggregate a Parquet trade tape to one VWAP and one volume per symbol per minute, in a single DuckDB query. The file path is a bound parameter. Despite the page slug, this is a time-bucket aggregation, not an as-of join.
- Category: Data Engineering for Quants
- Canonical URL: https://quantcore-labs.com/code/duckdb-asof-vwap/
- Part of: https://quantcore-labs.com/code/
- Python source: https://quantcore-labs.com/code/duckdb-asof-vwap/duckdb-asof-vwap.py