Search Shortcut cmd + k | ctrl + k
duckfn_quantstats

Complete quantstats HTML tearsheets from SQL - one long table in, one full report per instrument out, benchmarks included

Maintainer(s): shijianjs

Installing and Loading

INSTALL duckfn_quantstats FROM community;
LOAD duckfn_quantstats;

Example

-- demo/prices.csv is a committed snapshot of daily closes for GOOGL, MSFT and the S&P 500 index (SPX)
CREATE TABLE prices AS
SELECT * FROM read_csv('https://raw.githubusercontent.com/shijianjs/duckfn-quantstats/main/demo/prices.csv');

-- 1. The whole table in one call: one report per instrument, opened in the system default browser
--    once it has been generated
SELECT (r).symbol, (r).strategy_title, length((r).html) AS html_bytes, (r).file_path
FROM (
    SELECT unnest(qs_html_reports_by_prices(
               symbol, date, price,
               {'title': symbol,
                'strategy_title': symbol,
                'open_in_browser': true}::qs_html_report_options)) AS r
    FROM prices
);

-- 2. With a benchmark: SPX is just another symbol in the table (name it in the options),
--    and it gets no report of its own
SELECT (r).symbol, (r).file_path
FROM (
    SELECT unnest(qs_html_reports_by_prices(
               symbol, date, price,
               {'benchmark': ['SPX'],
                'benchmark_title': ['S&P 500'],
                'title': symbol,
                'strategy_title': symbol,
                'rf': 0.04,
                'open_in_browser': true}::qs_html_report_options)) AS r
    FROM prices
);

About duckfn_quantstats

Two aggregate functions that turn one date-ordered long table into a whole set of quantstats HTML tearsheets, one report per instrument (and one per benchmark, when the options name several):

Function Input
qs_html_reports(symbol, date, period_return, options) periodic returns
qs_html_reports_by_prices(symbol, date, price, options) prices or NAVs, differenced into returns inside the function

Both return the same STRUCT(symbol, benchmark, strategy_title, benchmark_title, html, file_path)[], so unnest(...) spreads it into rows, list_transform(...) picks fields, and (…)[1].html grabs a single report. No GROUP BY appears in the SQL: symbol is the grouping key and the function splits by it internally. Reports are ordered by symbol (and by the benchmark list within one symbol), independent of input order and thread count.

A benchmark is just another symbol of the same table, named in the benchmark option as a list (['SPX', 'NDX']); those symbols are input only and get no report of their own, so "one instrument against M benchmarks" comes back as M reports differing in the benchmark field. The options are a named STRUCT (qs_html_report_options, created at load time) evaluated per row, which is how each instrument gets its own title / strategy_title — build it out of the symbol column: {'title': symbol, 'strategy_title': symbol, 'benchmark': ['SPX']}::qs_html_report_options. Other keys: rf (annualized risk-free rate), periods_per_year, match_dates, output_dir and open_in_browser.

With output_dir each report is also written through DuckDB's VFS — local disk, s3://… once httpfs is loaded, and the wasm build's file system all take the same path — under a name the function generates (<time>-<strategy>-<benchmark>-<random>.html, nothing is ever overwritten), and the path it actually wrote comes back in file_path. With open_in_browser the reports are handed to the system default browser instead. Reports are a few hundred KB of HTML each (a dozen inline SVGs), so a directory or a browser tab is friendlier than unnest-ing them into a terminal.

Requires DuckDB 1.5 or newer: the host file system used for output_dir only reached DuckDB's C API in 1.5. On wasm, output_dir works and open_in_browser is ignored (there is no browser process to launch; the HTML string comes back to the host as it is).

Development notes, the full option table and the error paths: https://github.com/shijianjs/duckfn-quantstats.

Added Functions

function_name function_type description comment examples
qs_html_reports aggregate Renders one quantstats HTML report per symbol from a long table of periodic returns Groups by symbol internally, so the SQL needs no GROUP BY; a benchmark is just another symbol of the same table, serves as input only and gets no report of its own [SELECT unnest(qs_html_reports(symbol, trade_date, daily_return, NULL)) FROM daily_returns; SELECT unnest(qs_html_reports(symbol, trade_date, daily_return, {'benchmark': ['SPX'], 'title': symbol, 'output_dir': 'reports/'}::qs_html_report_options)) FROM daily_returns]
qs_html_reports_by_prices aggregate Renders one quantstats HTML report per symbol from a long table of prices or NAVs, differencing them into returns first The value column is a level, not a change: every symbol, the benchmark included, is converted with price_t / price_{t-1} - 1 before rendering [SELECT unnest(qs_html_reports_by_prices(symbol, date, price, NULL)) FROM prices; SELECT unnest(qs_html_reports_by_prices(symbol, date, price, {'benchmark': ['SPX'], 'title': symbol, 'output_dir': 'reports/'}::qs_html_report_options)) FROM prices]

Overloaded Functions

This extension does not add any function overloads.

Added Types

type_name type_size logical_type type_category internal
qs_html_report_options 0 STRUCT COMPOSITE false

Added Settings

This extension does not add any settings.