edgekit

edgekit.report

Self-contained HTML reports. Everything is inlined — the CSS in a <style> block, every chart a data:image/png;base64, URI — so the file opens anywhere with no network. Pure-Python f-strings; matplotlib is only ever reached through edgekit.viz.

What's inside. The chainable Report builder (every method returns self), the three_report_suite convenience that emits a linked Challenge / Live / Realistic trio, and three module-level HTML helpers (build_css, card_html, month_table_html) for when you want fragments rather than a whole page.

Module helpers#

build_css(theme: str = "dark") -> str                    # inline stylesheet for a theme
card_html(label, val, sub: str = "") -> str              # one KPI card (HTML-escaped)
month_table_html(monthly, theme: str = "dark") -> str    # year x month grid table with row totals
  • build_css — the inline stylesheet string for a theme ("dark" or "light").
  • card_html — one KPI card as an HTML fragment (label, value, optional sub-line; all HTML-escaped).
  • month_table_html — a year × month grid table (with row totals) from a monthly series.

Report (chainable builder)#

Assembles one self-contained HTML page. Every method returns self for chaining.

Report(title: str, meta: str = "", theme: str = "dark")   # theme in {"dark","light"} or ValueError
ParamTypeDefaultMeaning
titlestrPage title (the top headline).
metastr""Sub-line under the title (e.g. instruments · account size).
themestr"dark"Palette: "dark" or "light" (else ValueError).
MethodPurpose
header(title, meta="")Reset the page title / meta.
nav(links)Row of (label, href) cross-links.
section(title)An <h2> heading.
hero(label, value, sub="")The big highlighted headline number.
kpi_row(cards)Flex row of KPI cards; each card is (label, val[, sub]) or a dict.
card(label, val, sub="")A single KPI card.
table(headers, rows)A plain table (headers list, rows list of cell-lists).
month_table(monthly)Year x month grid with totals.
chart(fig, dpi=95)Embed a matplotlib figure as an inline base64 PNG (closes it).
caveat(text)A muted caveat box.
html_text(raw)Append a raw HTML fragment verbatim.
grid(figs, cols=2, dpi=95)Several figures in a responsive CSS grid (each inlined).
metrics_table(stats, title=None)A metrics dict rendered as a key/value table.
image(src)Embed an EXTERNAL PNG (path or bytes) as an inline data-URI.
gauntlet_panel(...)A standardized Validation section (perm_p / stress / beta / dsr / pbo).
render() -> strAssemble the full <!doctype html> string.
write(path) -> PathRender and write to path (returns the Path).
report.py
from edgekit.report import Report
from edgekit import viz

monthly = viz.trades_to_monthly(trades)
fig = viz.monthly_heatmap(monthly)

path = (Report("ORB — US100 M1", meta="Nasdaq intraday · $100k", theme="dark")
        .hero("Expectancy", "−0.214R", "net of costs")
        .kpi_row([("PF", "0.71"), ("Win", "39%"), {"label": "Trades", "val": "2,666"}])
        .section("Equity")
        .chart(fig)
        .month_table(monthly)
        .table(["Year", "Total R"], [[2023, -12.4], [2024, -9.1]])
        .caveat("Net-negative after costs; the gauntlet rejects it.")
        .write("orb_report.html"))
print(path)   # -> orb_report.html (self-contained, opens offline)
Fully offline by construction
The render asserts nothing external: CSS is inlined, charts are base64 data URIs. A finished report contains no http:// or https:// — you can email it, drop it on a share, or open it on a plane and it renders identically.

Composite blocks#

Four higher-level blocks build on the primitives above. Each still returns self.

grid#

Embed several matplotlib figures in a responsive CSS grid, each inlined as its own base64 PNG. Handy for a panel of diagnostics under one heading.

grid(figs, cols: int = 2, dpi: int = 95) -> Report
  • figs — an iterable of matplotlib figures (each is closed after encoding).
  • cols — grid columns (default 2).
  • dpi — render resolution per figure (default 95).

metrics_table#

Render a metrics dict (e.g. trade_stats / equity_stats) as a key/value table, with sensible int/float/bool/str formatting. An optional title emits an <h3> above it.

metrics_table(stats: dict, title: str | None = None) -> Report

image#

Embed an external PNG — a filesystem path or raw bytes — as an inline data-URI. Use this for a chart produced outside viz (e.g. one saved earlier with viz.save_png). Contrast with chart(fig), which takes a live matplotlib figure.

image(src) -> Report
  • src — a path (str / Path) read from disk, or raw bytes.

gauntlet_panel#

A standardized Validation section that renders whichever results you pass. KPI cards for the scalar checks and a cost-stress table for stress, plus the survivor rule (PF > 1 at 2x and 3x cost).

gauntlet_panel(perm_p=None, stress=None, beta=None, dsr=None, pbo=None) -> Report
ParamRendersVerdict rule
perm_pPermutation p cardPASS if < 0.01
stressCost-stress PF / EV-R tablePF > 1 at 2x and 3x
betaIs-it-beta (β) cardalpha if |β| < 0.2
dsrDeflated Sharpe cardPASS if > 0.95
pboPBO cardPASS if < 0.5
(Report("ORB — validation", theme="dark")
    .gauntlet_panel(perm_p=0.040,
                    stress={1: {"pf": 0.71, "ev_r": -0.214},
                            2: {"pf": 0.52, "ev_r": -0.402},
                            3: {"pf": 0.38, "ev_r": -0.560}},
                    beta=0.05, pbo=0.71)
    .write("orb_validation.html"))
# perm_p 0.040 -> FAIL vs 0.01; PF < 1 at every cost multiple.

tear_sheet#

The one-call full report. Assembles a complete standard tear sheet from a trade frame: a hero + KPI row from trade_stats, an equity+drawdown panel, a monthly-return heatmap, a return distribution and R histogram (in a two-up grid), and a metrics table. If account (dollars-per-R) is given it adds a dollar monthly heatmap too.

tear_sheet(trades: pd.DataFrame, account: float | None = None,
           title: str = "Strategy tear sheet", theme: str = "dark", out=None)
ParamTypeDefaultMeaning
tradespd.DataFrameCanonical trade frame (date + r).
accountfloat | NoneNoneDollars-per-R; if given, adds a dollar monthly heatmap.
titlestr"Strategy tear sheet"Page title.
themestr"dark"Palette: "dark" or "light".
outstr | Path | NoneNoneIf given, writes there and returns the Path; else returns the HTML string.
tear_sheet.py
from edgekit.report import tear_sheet

# one call: trade frame in, self-contained HTML out
path = tear_sheet(trades, account=500.0,
                  title="ORB — US100 M1", theme="dark", out="orb_tearsheet.html")
print(path)   # -> orb_tearsheet.html

# omit out= to get the HTML string back instead of writing
html = tear_sheet(trades)
Reach for tear_sheet first
For the standard picture, tear_sheet is the fast path — one call covers equity, drawdown, distribution and the metrics table. Drop down to the Report builder when you need a custom layout, cross-linked pages, or a gauntlet_panel.

three_report_suite#

Emit the linked Challenge / Live / Realistic trio in one call. Sizes the trade R-stream to dd_budget of account, then renders three cross-linked self-contained pages: an aggressive challenge view, a live-ceiling view, and a haircut-adjusted realistic view.

three_report_suite(trades: pd.DataFrame, account: float, out_dir,
                   title_prefix: str = "Strategy", theme: str = "dark",
                   dd_budget: float = 0.095, haircut: float = 0.85, split: float = 0.90) -> list[Path]
ParamTypeDefaultMeaning
tradespd.DataFrameCanonical trade frame (date + r).
accountfloatAccount size in dollars.
out_dirstr | PathDirectory the three HTML files are written into.
title_prefixstr"Strategy"Prefix for the three page titles.
themestr"dark"Palette for all three pages.
dd_budgetfloat0.095Drawdown budget the R-stream is sized to.
haircutfloat0.85Multiplier applied for the realistic page (the honest haircut).
splitfloat0.90IS/OOS split fraction used inside the suite.

Returns a list[Path] — the three written file paths.

from edgekit.report import three_report_suite
paths = three_report_suite(trades, account=100_000, out_dir="reports/", title_prefix="ORB")
# -> [reports/orb_challenge.html, reports/orb_live.html, reports/orb_realistic.html]
!The realistic page is the one to plan around
A drawdown-matched backtest is a ceiling, not an expectation. The suite always emits the haircut-adjusted realistic page precisely so the number you plan around is the one you can actually trade — not the aggressive challenge headline.

See also#