报告、运行时与扩展
这里有两层相互衔接的东西:所有后端都会渲染的类型化报表模型,以及把操作变成目录条目与执行结果的运行时层。两者都偏好显式契约而不是便利。
一份文档,多种后端
report_document 是构建报表的唯一值:
fincore::report_document document;
document.domain = "portfolio";
document.title = "Monthly review";
document.metadata = {{"source", "fixture"}};
文档包含多个 section;每个 section 包含 metrics、tables、series、notes、units 与 legends。值是带类型的单元格 —— 文本、数值、布尔或缺失 —— 因此后端无需猜测某列是否为数值。
后端 |
函数 |
说明 |
|---|---|---|
HTML |
|
|
XLSX |
|
每个 section 一个工作表;单元格使用电子表格命名与内联字符串 |
|
|
|
Plotly |
|
返回带 |
Bokeh |
|
bundle 形状与 Plotly 相同 |
静态坐标轴 |
|
调用方提供的坐标轴标记为非拥有 |
对确定性后端而言,同一文档渲染两次会产出字节完全一致的结果。这正是报表可以安全哈希与比较的原因,也是工作簿写出器固定 ZIP 时间戳而不是打上当前时间的原因。
Artifact bundle
绘图与 PDF 渲染器返回 report_artifact_bundle:
metadata始终携带backend与report_digest;report_digest()是文档的语义摘要,因此同一文档的不同后端彼此一致。artifacts携带具名输出(figure、file、axis:<section>.<series>),并带name、path、owned与content。find(name)在名称不存在时返回nullptr,因此不加检查的解引用是编程错误而不是静默默认值。调用方提供的坐标轴,以及
write_pdf产出的文件,都是非拥有的:生命周期由调用方负责。
转义与非有限值
两套转义共存,因为有两个输出通道。标记文本走 HTML 实体;嵌入脚本元素的序列载荷走 JSON 转义,因此 <、>、& 变成 <、>、&,且没有非 ASCII 码点以未转义形式进入脚本源。
非有限数值不会打印成 NaN 文本。通道能承载空隙时该值变成 null;几何无法承载时该点被丢弃或折叠为 0。渲染器会说明自己用哪一种,因为把无穷静默画成 0 比留出空隙更糟。
运行时目录
操作是被描述的,而不是运行时按名字发现的。每个模块暴露自己的描述符列表 —— metrics_operations()、risk_operations()、portfolio_operations() 等 —— all_operations() 按固定顺序拼接它们。该顺序是摘要的一部分,因此既不是字母序也不是偶然得到的。
operation_catalog 注册规格并解析请求;未知或歧义的名字会抛 operation_resolution_error。请求产生 execution_plan,执行后得到 runtime_result,其载荷是带类型的 runtime_value 而不是字符串映射。
会话与缓存
analysis_context 缓存确定性计算。summary() 在第二次调用时返回同一个映射,而 computation_count(key) 保持为 1 —— 证明缓存被使用的是计数,而不是相等性。由于缓存可变,一个上下文在构造上是单线程的;帧仍然可以共享。
analysis_session 拥有自己的输入与输出,并在 close() 之后拒绝调用。重复关闭是安全的。即使每个可调用对象都完整,扩展合并也只发生一次,因此会话不会半途应用一组注册。
扩展
扩展发现使用扩展头文件里写明的原生快照与 hook ABI。读快照时有两件事重要:
hook 携带事件名与优先级:优先级更低者先运行,事件与优先级都相同的 hook 按 fingerprint 排序。因此仅按优先级排序整个注册表会打乱同一事件内的顺序。
快照摘要覆盖的是 ASCII 转义后的载荷,而不是宿主编码文本,因此非 ASCII 标识符只有一种规范字节形式。
绘图适配器(ImPlot、基于 GLFW 的示例)不属于核心 CMake 消费契约;只有启用相应选项时才会构建。
最小报表
#include <fincore/report/html.hpp>
#include <fincore/report/models.hpp>
#include <fincore/report/xlsx.hpp>
fincore::report_document document;
document.domain = "demo";
document.title = "Demo";
fincore::report_section section;
section.key = "summary";
fincore::report_metric metric;
metric.key = "sharpe";
metric.label = "Sharpe";
metric.number = 1.25;
section.metrics.push_back(metric);
document.sections.push_back(section);
const std::string html = fincore::render_html(document, {.language = "en"});
const std::string workbook = fincore::render_xlsx(document);
两个字符串都是完整文件;不需要再生成或套用任何模板。