# Report

> Use when tasks need complete HTML research reports, HTML dashboards, PNG chart helpers, or files under the research reports directory.

- Skill: `quantskills/report` (Agent Skill, multi-file: 12 files)
- Install (CLI): `npx skillmds@latest add quantskills/report`
- Raw SKILL.md: https://api.skillmd.com/api/skills/quantskills/report/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: quantskills (https://skillmd.com/u/quantskills)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/quantskills/report

---


# 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

```python
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,  # from VectorBacktester, do not invent numbers
    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:

```text
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

```python
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 }}`.

