Skip to content

tearsheet

report.to_html() / report.show(): the interactive tearsheet — one self-contained HTML file with the tape, every fill marked, the equity curve against the MLL floor, and the text render's stats.

tearsheet

Interactive HTML tearsheet: the text report, plus the charts it cannot print.

:func:render_html turns a finished :class:~topstep_backtest.harness.Report into ONE self-contained HTML document — candlestick price panes with entry/exit markers, the equity curve with the trailing MLL floor and the intrabar equity envelope, a daily P&L histogram, an R-multiple distribution, and every stats section the text render prints, label for label. The file needs no server, no CDN and no network: the charting library (TradingView Lightweight Charts™, Apache-2.0, vendored under _assets/ with its license) and all styling are inlined, so the document opens offline and can be archived next to a run.

The render is a pure function of the frozen report data. No wall clock, no unordered iteration: rendering the same Report twice yields byte-identical HTML, the same contract str(report) keeps. The provenance line every text report carries is stamped on the page footer for the same reason — a chart gets screenshotted out of context even more readily than a verdict line.

This module is where Decimal leaves the money path. Everything shown in a stats tile is formatted in Python by the exact helpers Report.__str__ uses, so gross/net basis labels cannot drift from the text render; only chart series cross to the JavaScript side as raw decimal strings, converted to floats by the chart layer at the last possible moment (floats are a display concern here, never an accounting one).

Two tabs, one payload. Results is the finished run — the tape, equity against the floor, daily P&L, the R histogram, the stat cards. Replay appears only when the report carries a recording and holds the bar-by-bar cockpit: its own charts (veiled after the cursor, so nothing past the strategy's knowledge is drawn) beside the settled state and the running stats — regrouped into by-type cards (_REPLAY_CARDS), shown one at a time behind filter chips — with the event log across the full width beneath, laid out to be read in one viewport instead of scrolled between. Its charts are separate from the results tab's on purpose — a veiled chart is a run mid-flight, and the results sheet is the run that finished.

Reached via :meth:Report.to_html(path) <topstep_backtest.harness.Report.to_html> (writes the named file, the only disk write) or :meth:Report.show() <topstep_backtest.harness.Report.show> (temp file + default browser, the write is documented there).

SCHEMA_VERSION module-attribute

SCHEMA_VERSION = 2

Version of the embedded JSON payload contract (bumped on breaking changes).

ReplaySpec module-attribute

ReplaySpec = Literal['auto', 'full', 'off'] | tuple[int, int]

How much of a recording the tearsheet embeds — see Report.to_html.

REPLAY_AUTO_FRAME_LIMIT module-attribute

REPLAY_AUTO_FRAME_LIMIT = 20000

Frames replay="auto" will embed before falling back to a window.

Payload weight is roughly linear in frames (indicator strings dominate), and past this many the document crosses from "large" to "hostile to browsers". "full" overrides deliberately; the auto window centres on the MLL breach when there is one — the moment worth stepping through — and otherwise takes the tail.

render_sweep_html

render_sweep_html(sweep: WindowSweep | SpacedSweep) -> str

Render a sweep as one self-contained HTML document.

Pure function of the frozen sweep data: rendering twice is byte-identical. Everything — styling, charts, the interaction script — is inlined; the document opens offline and can be archived next to a run.

Source code in src/topstep_backtest/tearsheet/sweep.py
def render_sweep_html(sweep: WindowSweep | SpacedSweep) -> str:
    """Render a sweep as one self-contained HTML document.

    Pure function of the frozen sweep data: rendering twice is byte-identical.
    Everything — styling, charts, the interaction script — is inlined; the
    document opens offline and can be archived next to a run.
    """
    windows = sweep.windows
    passed = sum(1 for w in windows if w.passed)
    total = len(windows)
    spaced = isinstance(sweep, SpacedSweep)

    if spaced:
        kind_line = (
            f"{total} attempts of {sweep.window_days} trading days, start days spread "
            f"evenly across {sweep.source_days} trading days — overlap allowed"
        )
        evidence = sweep.effective_independent_windows
        caveat = (
            f"Overlapping attempts share data: this sweep's rates rest on about "
            f"{evidence} independent window(s), not {total}. More periods sharpen the "
            "start-date picture below; they add no evidence to the rate."
        )
    else:
        kind_line = (
            f"{total} consecutive disjoint attempts of {sweep.window_days} trading days "
            f"from {sweep.source_days} trading days"
        )
        caveat = (
            f"{total} attempts is a small sample — read the rate as an estimate off "
            f"{total} observations, not a probability."
        )

    cold_note = ""
    if sweep.cold_start_windows:
        cold_note = (
            f"{sweep.cold_start_windows} attempt(s) started with indicators not yet warm "
            "(no earlier data to warm from). Cold attempts under-trade, which biases the "
            "pass rate DOWN; dashed outlines mark them in the timeline."
        )

    any_headroom = any(w.min_floor_headroom is not None for w in windows)
    title = html.escape(f"Combine sweep: {passed}/{total} passed")
    css = (_ASSETS / "sweep.css").read_text("utf-8")
    app = (_ASSETS / "sweep.js").read_text("utf-8")

    parts = [
        "<!DOCTYPE html>\n",
        '<html lang="en">\n<head>\n<meta charset="utf-8">\n',
        '<meta name="viewport" content="width=device-width, initial-scale=1">\n',
        "<!--\n  ",
        PROVENANCE,
        "\n-->\n",
        "<title>",
        title,
        "</title>\n<style>\n",
        css,
        "\n</style>\n</head>\n<body>\n",
        '<div class="wrap">\n',
        "<header>",
        f"<h1>{passed} / {total} attempts passed</h1>",
        f'<p class="kind">{html.escape(kind_line)}</p>',
        "</header>\n",
        f'<div class="banner">{html.escape(caveat)}</div>\n',
        (f'<div class="banner banner-cold">{html.escape(cold_note)}</div>\n' if cold_note else ""),
        '<section class="tiles">',
        _tiles(sweep),
        "</section>\n",
        "<section><h2>Outcomes</h2>",
        _stack_bar(windows),
        "</section>\n",
        "<section><h2>Attempts on the calendar</h2>",
        '<p class="hint">Each bar is one attempt over its actual dates; stacked bars share '
        "the same market weeks. Hover to link with the table.</p>",
        _timeline_svg(windows),
        "</section>\n",
        "<section><h2>Cumulative P&amp;L per attempt, from $0</h2>",
        '<p class="hint">Trading day of the attempt along the bottom. The spread of these '
        "paths IS the start-date sensitivity.</p>",
        _paths_svg(windows),
        "</section>\n",
    ]
    if any_headroom:
        parts.extend(
            (
                "<section><h2>Closest approach to the MLL floor</h2>",
                '<p class="hint">A short bar passed (or survived) by luck: the distance shown '
                "is all the room that ever existed.</p>",
                _headroom_svg(windows),
                "</section>\n",
            )
        )
    parts.extend(
        (
            "<section><h2>Every attempt</h2>",
            '<p class="hint">Click a column to sort. Consistency is the dollar slack in the '
            "50% rule at window end; negative means the money was made and would have been "
            "refused.</p>",
            _table(windows),
            "</section>\n",
            f"<footer><p>{html.escape(PROVENANCE)}</p></footer>\n",
            "</div>\n<script>\n",
            app,
            "\n</script>\n</body>\n</html>\n",
        )
    )
    return "".join(parts)

render_html

render_html(report: Report, *, replay: ReplaySpec = 'auto', confidence: MonteCarloConfidence | None = None, crosscheck: CrossCheck | None = None) -> str

Render report as one self-contained interactive HTML document.

Pure function of the frozen inputs: rendering twice is byte-identical. The returned string embeds the payload JSON, the vendored charting library and all styling — nothing is fetched at view time. replay selects how much of a recording is embedded (see Report.to_html); it is inert when the report carries none.

confidence (a :func:~topstep_backtest.metrics.confidence.mc_confidence bundle) adds the Monte-Carlo cards — estimate with its CI, block-length sensitivity, per-year strata; crosscheck (:func:~topstep_backtest.metrics.confidence.crosscheck) adds the bootstrap-vs-real-windows card. Both are computed by the caller, never here: a render must stay a pure formatting pass, and the simulations they involve are neither cheap nor this module's business.

Source code in src/topstep_backtest/tearsheet/__init__.py
def render_html(
    report: Report,
    *,
    replay: ReplaySpec = "auto",
    confidence: MonteCarloConfidence | None = None,
    crosscheck: CrossCheck | None = None,
) -> str:
    """Render ``report`` as one self-contained interactive HTML document.

    Pure function of the frozen inputs: rendering twice is byte-identical.
    The returned string embeds the payload JSON, the vendored charting library
    and all styling — nothing is fetched at view time. ``replay`` selects how
    much of a recording is embedded (see ``Report.to_html``); it is inert when
    the report carries none.

    ``confidence`` (a :func:`~topstep_backtest.metrics.confidence.mc_confidence`
    bundle) adds the Monte-Carlo cards — estimate with its CI, block-length
    sensitivity, per-year strata; ``crosscheck``
    (:func:`~topstep_backtest.metrics.confidence.crosscheck`) adds the
    bootstrap-vs-real-windows card. Both are computed by the caller, never
    here: a render must stay a pure formatting pass, and the simulations they
    involve are neither cheap nor this module's business.
    """
    payload = _build_payload(report, replay, confidence, crosscheck)
    # "<" only occurs inside JSON string values; rewriting it to the JSON-legal
    # escape \\u003c defuses every dangerous sequence (script close, comment
    # open) at once, and JSON decoders undo it transparently.
    data = msgspec.json.encode(payload).decode("utf-8").replace("<", "\\u003c")
    library = (_ASSETS / "lightweight-charts.standalone.production.js").read_text("utf-8")
    app = (_ASSETS / "tearsheet.js").read_text("utf-8")
    css = (_ASSETS / "tearsheet.css").read_text("utf-8")
    title = html.escape(f"Topstep Combine {payload.size}: {payload.verdict}")

    parts = [
        "<!DOCTYPE html>\n",
        '<html lang="en">\n<head>\n<meta charset="utf-8">\n',
        '<meta name="viewport" content="width=device-width, initial-scale=1">\n',
        "<!--\n  ",
        payload.provenance,
        "\n  Embeds TradingView Lightweight Charts(TM), (c) TradingView, Inc.,\n"
        "  Apache License 2.0 — see topstep_backtest/tearsheet/_assets/ for the\n"
        "  full license and NOTICE.\n-->\n",
        "<title>",
        title,
        "</title>\n<style>\n",
        css,
        "\n</style>\n</head>\n<body>\n",
        '<div class="wrap">\n',
        '<header id="verdict-header"><h1 id="verdict-title"></h1>'
        '<p id="verdict-reason"></p></header>\n',
        '<div id="provisional-banner" hidden></div>\n',
        # Two views over one payload: the classic sheet, and (only when a
        # recording is embedded) the replay cockpit, which needs the whole
        # viewport to put chart, state and log side by side. The tab bar and
        # the panels' contents are filled in by the app script.
        '<nav id="tabs" hidden></nav>\n',
        "<main>\n",
        '<div id="tab-results" class="tab-panel">\n',
        '<section id="price-panes"></section>\n',
        '<section id="equity-pane"></section>\n',
        '<section id="replay-hint"></section>\n',
        '<section id="daily-pane"></section>\n',
        '<section id="r-hist"></section>\n',
        '<section id="stats"></section>\n',
        "</div>\n",
        '<div id="tab-replay" class="tab-panel" hidden>\n',
        '<section id="replay-pane"></section>\n',
        "</div>\n",
        "</main>\n",
        '<footer id="provenance"><p id="provenance-text"></p>\n',
        '<p class="attribution">Charting by <a href="https://www.tradingview.com/">'
        "TradingView Lightweight Charts&trade;</a></p></footer>\n",
        "</div>\n",
        '<script type="application/json" id="data">',
        data,
        "</script>\n<script>\n",
        library,
        "\n</script>\n<script>\n",
        app,
        "\n</script>\n</body>\n</html>\n",
    ]
    return "".join(parts)