Options
The fourth argument of both functions is a qs_html_report_options STRUCT — a named type the
extension creates at load time. Every field is nullable; keys you omit take their default:
| Field | Type | Default | Description |
|---|---|---|---|
title | VARCHAR | 'Strategy Tearsheet' | Report title |
strategy_title | VARCHAR | the symbol | Strategy display name; falls back to the symbol |
benchmark_title | VARCHAR[] | the benchmark symbol | Benchmark display name (presentation only): a list, paired with benchmark by index. An entry that was not given (shorter list, NULL, empty string, or the whole key absent) falls back to the benchmark symbol that report uses; extra entries are ignored |
benchmark | VARCHAR[] | NULL | Which symbols are benchmarks (a list, whose order is the order of the reports); they are input only and get no report. Even a single benchmark is written ['SPX'] |
rf | DOUBLE | 0.0 | Risk-free rate, annualized (0.04 = 4%), matching quantstats' rf convention |
periods_per_year | UINTEGER | 252 | Periods per year; must be greater than 0 |
match_dates | BOOLEAN | true | Whether to align the start dates of strategy and benchmark |
lang | VARCHAR | NULL | Translate the report into this language (see Translation); must have entries in the translation table. NULL = no translation at all, 'en' = keep the English text and add English notes. Spelled lang because language is a SQL keyword |
output_dir | VARCHAR | NULL | Also write every report into this local directory, with file names generated by the function (see Output and browser); a wasm build writes nothing |
open_in_browser | BOOLEAN | false | Open the reports in the system default browser; with no output_dir it writes a temporary file first (see Output and browser) |
Apart from the two display names, the defaults come straight from quantstats-rs'
HtmlReportOptions::default(); this extension does not invent a second set.
The two display names are the exception
They are what makes dozens of reports out of one call usable: the default 'Strategy' is identical
for every one of them, so the legend could not tell them apart and the temporary file names would
share one useless prefix. They therefore fall back to a name that comes from the data (the symbol,
the benchmark symbol), which keeps the report legend, the temporary file name and the returned
strategy_title / benchmark_title in agreement. title is yours as well — it is not translated
either, so Translation only touches the report's own fixed texts.
The options are a per-row column
Each symbol uses the copy from its first row, so building the struct out of the symbol column is
what gives every instrument its own title and display name — and the benchmark list its own display
names, paired by index:
WITH prices AS (
SELECT * FROM read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv')
)
SELECT (r).symbol, (r).strategy_title, (r).benchmark, (r).benchmark_title,
length((r).html) AS html_bytes
FROM (
SELECT unnest(qs_html_reports_by_prices(
symbol, date, price,
{'benchmark': ['SPX'],
'benchmark_title': ['S&P 500'],
'title': symbol, -- per-row: the report's own title
'strategy_title': symbol, -- per-row: and the legend's label
'rf': 0.04,
'periods_per_year': 252}::qs_html_report_options)) AS r
FROM prices
)
ORDER BY (r).symbol;
strategy_title and benchmark_title come back in the row, so whatever the legend shows can be read
without parsing the HTML — and they are the very names the generated file name uses, too.
One symbol's options have to agree row by row; benchmark additionally has to be the same list for
every instrument in the call (same entries, same order — a disagreement is an error), otherwise
"which one is the benchmark" would have no single answer. Error paths has a
runnable example of each.
Which entries are validated
benchmark decides who is a benchmark and who gets a report, so it is strict: an element may not
be NULL, may not be an empty string and may not repeat inside one list. benchmark_title is
presentation only, so it is lenient (missing entries fall back, extra entries are ignored).
periods_per_year = 0 and output_dir = '' are reported as configuration errors as well — all of
them before anything is rendered or any file-system call happens. The full list is on
Error paths.
rf is annualized
0.04 means 4% and is converted to a per-period rate inside the report; the crate has two
conversions that differ slightly — Sharpe (and rolling Sharpe / Sortino) uses
(1 + rf)^(1/periods_per_year) - 1, while PSR / Sortino in the metrics table use
rf / periods_per_year. Both collapse to 0 when rf = 0 (the default).