| name | report |
| description | Use when tasks need complete HTML research reports, HTML dashboards, PNG chart helpers, or files under the research reports directory. |
Report Skill
Render research outputs into self-contained HTML using Jinja2 templates and
matplotlib charts. Install with uv sync --extra report. Default output uses
QUANTSPACE_REPORTS_ROOT when set, otherwise the workspace reports/ path.
Complete research archives are HTML only. Do not write them as Markdown or
PDF. Do not use a database. Other skills must not write HTML; they return
objects, and the caller fills ResearchReport then calls
write_research_bundle.
Two output paths
| Path | Entry | Output | Use for |
|---|
| Complete research archive | ResearchReport + write_research_bundle | reports/<namespace>/<slug>/index.html | Takeaway research document, including public examples |
| Dashboard preview | ReportRenderer + factor_report / backtest_report / signal_digest | one HTML file | Quick look, not an archive |
Charts are inlined as base64 data URIs via the png_data_uri Jinja filter.
Complete research archive
from skills.report import (
ReportFigure,
ReportTable,
ResearchReport,
charts,
write_research_bundle,
write_research_catalog,
)
equity_png = charts.plot_backtest_performance(result_df, title="Performance")
report = ResearchReport(
namespace="lesson_09",
slug="if_ma10_atr",
title="IF MA10 + ATR",
question="Does this rule beat buy-and-hold under the stated costs?",
universe=["CFFEX.IF99"],
frequency="1d",
sample_start="2024-01-01",
sample_end="2026-07-01",
in_sample_end=None,
out_of_sample_start=None,
hypothesis="Close below MA10 enters; ATR stop only ratchets up.",
method_notes=["Rule from strategies.time_series.rules."],
execution={
"trade_at": "close",
"signal_lag": 1,
"commission": 0.0002,
"slippage_bp": 2.0,
"return_mode": "forward",
},
metrics=execution.metrics,
metrics_source="BacktestResult.metrics",
figures=[ReportFigure(name="equity", caption="Equity and drawdown", png=equity_png)],
tables=[],
caveats=["Historical result only; not a live trading recommendation."],
next_steps=["Add cost sensitivity."],
reproduce_command="uv run python -m strategies.time_series.workflows.run_demo",
visibility="private",
domain="time_series",
)
study_dir = write_research_bundle(report)
write_research_catalog()
Directory contract:
reports/<namespace>/<slug>/index.html
reports/<namespace>/<slug>/params.json
reports/catalog.html
reports/catalog.json
index.html is the human-readable nine-section report. params.json is the
catalog sidecar. list_research_studies only accepts folders that have both
files. CSV-only experiment folders are ignored. write_research_bundle does
not write the catalog; call write_research_catalog after a batch.
Required HTML sections: 研究问题, 数据与样本, 假设与方法, 执行约定,
证据与指标, 图表, 对照与稳健性, 限制与下一步, 复现与产物.
If there is no comparison or robustness evidence, section 7 still renders
本报告未做.
Hard rules:
- Fill
metrics from BacktestResult, CSV, JSON, or result_df. Never invent Sharpe.
metrics_source is required.
namespace and slug are safe path segments only.
- Default
visibility="private". Do not git-add private studies.
- Public examples use
namespace="strategy_examples" and visibility="public_example". They still write reports/<namespace>/<slug>/.
- Do not import
strategies/ from this skill.
- Do not export PDF.
Public examples from scripts/run_strategy_reports use the same archive
contract: namespace="strategy_examples", visibility="public_example",
kind="public_example". README gallery PNGs are extra sidecar files written
by that script, not by write_research_bundle.
Dashboard preview
from skills.report import ReportRenderer, charts
renderer = ReportRenderer()
ranking_png = charts.plot_factor_ranking(ranking_df, value_col="IC_IR")
html = renderer.render(
"factor_report",
{
"title": "Macro universe — weekly factor screen",
"namespace": "macro_weekly",
"n": 5,
"as_of": "2026-05-08",
"ranking_chart": ranking_png,
"ranking_html": ranking_df.to_html(),
},
)
path = renderer.save(html, "macro_weekly_2026-05-08.html")
Pass a template name with or without .html. Relative output paths resolve
against reports/; absolute paths are respected as-is.
Available charts
| Function | Returns |
|---|
plot_equity_curve(returns, title) | Cumulative (1+r).cumprod() equity curve |
plot_backtest_performance(result_df, title) | Backtest equity curve plus drawdown |
plot_factor_diagnostics(ic_series, ic_stats, group_returns, turnover, title, rolling_ir_window) | IC, rolling IR, layered NAV, and turnover dashboard |
plot_ic_heatmap(ic_df, title) | RdBu_r symmetric heatmap (rows=factors, cols=namespaces/periods) |
plot_rolling_pair_correlation(history, title) | Small-multiple histories from Analyze's tidy rolling factor correlations |
plot_horizon_ic(summary, factors, segment) | Multi-factor Horizon IC term structure |
plot_lagged_ic(summary, factors, horizons, segment) | Four-panel signal-delay decay curves |
plot_rebalance_comparison(comparison, segment, selected_days) | Net Sharpe and turnover by rebalance interval |
plot_factor_weight_history(factor_weights, method, start) | Stacked dynamic factor weights |
plot_equity_comparison(equities, start, title) | Rebased equity curves for multiple combination methods |
plot_factor_ranking(ranking_df, value_col, label_col, title, top_n) | Horizontal bar chart of top factors, colored by sign |
plot_regime_states(prices, states, title) | Price line with colored bands per regime |
All helpers return bytes (PNG). The headless Agg backend is pinned at
import time so reports render without a display.
Importing skills.report.charts (or calling charts.configure_cjk_matplotlib())
selects a system Han font so Chinese titles in PNG files render on macOS
(PingFang / Hiragino Sans GB), Windows (Microsoft YaHei / SimHei from
%WINDIR%\\Fonts), and Linux (Noto / Source Han / WenQuanYi). On Windows the
file is registered with fontManager.addfont and applied by file path, not by
font name alone — that is what makes msyh.ttc work with matplotlib. Custom
figures should go through charts.fig_to_png(fig) instead of savefig. If no
CJK font is installed (for example an English Windows image without the
Chinese language pack), charts still save; glyphs may fall back to boxes.
Windows and paths
- Matplotlib Chinese: load
msyh.ttc / simhei.ttf from %WINDIR%\\Fonts
(case-insensitive), register the file, set Microsoft YaHei / 微软雅黑
aliases, and disable axes.unicode_minus.
- Write HTML/JSON/Markdown as UTF-8 with
\n newlines. Catalog reads tolerate
a UTF-8 BOM (utf-8-sig).
- Study directories use
pathlib.Path (reports_root / namespace / slug).
- Catalog
href values are POSIX (namespace/slug/index.html) so they work
in browsers on Windows.
Template conventions
- Inline CSS only — reports must render standalone without external assets.
- Header band uses
#4f81bd; table header background #f4f4f4.
- Body font stack includes PingFang / YaHei / Noto so HTML Chinese is visible
on macOS and Windows.
- Every HTML report includes a relative link back to
catalog.html
(../../catalog.html from namespace/slug/index.html).
- Optional chart blocks are wrapped in
{% if chart %} … {% endif %}.
- Safe-render pre-built tables via
{{ table.html | safe }}.