跳到主要内容

函数

两个聚合函数名字、各一个签名,都把「一张长表」归约成整套报告:

签名输入返回
qs_html_reports(symbol, date, period_return, options)收益率序列STRUCT(symbol, benchmark, strategy_title, benchmark_title, html, file_path)[]
qs_html_reports_by_prices(symbol, date, price, options)价格/净值序列同上;函数内部先换算成收益率

下面每一块都能在你的浏览器里跑,读的是本站发布的演示快照(GOOGL、MSFT 与标普 500 指数, 各 1435 个交易日)—— 把 read_csv(…) 换成你自己的表,查询的形状完全一样。

要点​

  • 一次调用出整套报告。 SQL 里不写 GROUP BY:symbol 列就是分组依据,函数内部按它分组, 每个 symbol 渲染一份完整报告。100 个标的就是 100 份完整报告(每份内嵌十几张 SVG),耗时与内存随标的 数线性增长 —— 这是预期行为,不是性能 bug。
  • 返回的是一个数组,每个元素是 {symbol, benchmark, strategy_title, benchmark_title, html, file_path}。 unnest(...) 把它铺成行,list_transform(...) 只取需要的字段,也可以 (qs_html_reports(...))[1].html 直接取某一份。
  • 顺序按 symbol 升序,同一标的内按 benchmark 列表给出的顺序;与输入顺序、线程数都无关。
  • 基准是表里一个或多个普通的 symbol:配置里的 benchmark 是列表(['SPX', 'NDX']),列到的 symbol 只作输入、不出现在结果里。报告只能带一个基准,所以「一个标的对 M 个基准」就是M 份报告: 同一 symbol 出现 M 行,靠 benchmark 字段区分(一个基准多个标的则只是每个标的各出一份)。
  • symbol 是 VARCHAR,date 是 DATE,period_return 是按周期计的收益率(DOUBLE),price 是当天的价格或净值(DOUBLE)。这四者任一为 NULL 的行会被整行跳过(symbol 是空串的行同样 跳过),与其它 SQL 聚合函数一致。
  • 配置参数 options 固定在参数列表最后且必给 —— 不需要配置就写 NULL。它是可空配置,类型是 加载期建好的命名 STRUCT 类型 qs_html_report_options。
  • 配置按行求值(见配置字段),所以「每个标的一套标题 / 显示名 / 落盘目录」就是用 symbol 列把配置拼出来。
  • SQL 里不需要 ORDER BY:聚合内部只做拼接,排序交给报告自己去排。(示例里的 ORDER BY 只是让 你看到的行整齐一点。)
  • 名字为什么是两个:(symbol, date, price, options) 与 (symbol, date, period_return, options) 的类型 序列完全一样(VARCHAR, DATE, DOUBLE, STRUCT),同一个名字下无法分派。

从长表到返回数组,整次调用是这样走的:

用法​

整张表带基准 —— 最常见的情况:

WITH prices AS (
SELECT * FROM read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv')
)
SELECT (r).symbol, (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,
'strategy_title': symbol}::qs_html_report_options)) AS r
FROM prices
)
ORDER BY (r).symbol;

一个标的、不带基准 —— 先过滤:

WITH prices AS (
SELECT * FROM read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv')
WHERE symbol = 'GOOGL'
)
SELECT (r).symbol, (r).benchmark, length((r).html) AS html_bytes
FROM (
SELECT unnest(qs_html_reports_by_prices(symbol, date, price, NULL)) AS r
FROM prices
);

一个标的对两个基准 —— benchmark 列表的顺序就是报告顺序:

WITH prices AS (
SELECT * FROM read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv')
WHERE symbol IN ('GOOGL', 'SPX', 'MSFT')
)
SELECT (r).symbol, (r).benchmark, length((r).html) AS html_bytes
FROM (
SELECT unnest(qs_html_reports_by_prices(
symbol, date, price,
{'benchmark': ['SPX', 'MSFT'], 'title': symbol}::qs_html_report_options)) AS r
FROM prices
)
ORDER BY (r).benchmark;

收进来的不是价格而是收益率:

WITH prices AS (
SELECT * FROM read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv')
),
returns AS (
SELECT symbol, date,
price / lag(price) OVER (PARTITION BY symbol ORDER BY date) - 1.0 AS period_return
FROM prices
)
SELECT (r).symbol, length((r).html) AS html_bytes
FROM (
SELECT unnest(qs_html_reports(symbol, date, period_return, NULL)) AS r
FROM returns
)
ORDER BY (r).symbol;

只要清单、不把 HTML 拖出来(落盘见落盘与浏览器):

WITH prices AS (
SELECT * FROM read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv')
)
SELECT list_transform(
qs_html_reports_by_prices(symbol, date, price,
{'benchmark': ['SPX'], 'title': symbol}::qs_html_report_options),
lambda x: {'symbol': x.symbol, 'benchmark': x.benchmark, 'bytes': length(x.html)}) AS reports
FROM prices;

几个必须知道的行为​

  • 配置的 struct 字面量必须显式写 ::qs_html_report_options。 不写的话它是匿名的 STRUCT(title VARCHAR),匹配不上任何签名,DuckDB 会直接说找不到函数。
  • '...'::JSON::qs_html_report_options 要把 10 个键写全(DuckDB 的 JSON→STRUCT 转换不允许缺键), 所以推荐直接用 struct 字面量。
  • benchmark 写的是 symbol 名(列表),不是值。 每一项都必须是表里 symbol 列的某个取值,整次调用 一致;列到的 symbol 只当基准,不出现在返回的数组里。一个基准也要写成 ['SPX']。
  • benchmark_title 也是列表,按下标与 benchmark 对齐:['S&P 500', 'Nasdaq 100'] 分别对应第一个与 第二个基准。它只是展示,所以很宽松 —— 缺项(列表短了、NULL、空串)就退回那一份报告所用的基准 symbol, 多出来的表项直接忽略。
  • wasm 下既不落盘也不开浏览器:那边整个跳过文件操作(file_path 回来是 NULL), open_in_browser 也不做任何事 —— 没有浏览器进程可启动。见落盘与浏览器。
  • 演示快照很适合拿来试这些 —— 直接用 read_csv('https://shijianjs.github.io/duckfn-quantstats/demo/prices.csv') 读本站这份即可; 里面有什么见演示数据。