From e73d224cda8a7e18b817c4e13145dcc237d41a06 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Jul 2026 11:32:05 +0000 Subject: [PATCH] release: v0.12.3 --- pyproject.toml | 2 +- python/manifoldbt/plot/_decimate.py | 2 +- python/manifoldbt/plot/_theme.py | 10 +++--- python/manifoldbt/plot/_utils.py | 2 +- python/manifoldbt/plot/_window.py | 2 +- python/manifoldbt/plot/research.py | 51 ++++++++++++++++++++++------- python/manifoldbt/plot/tearsheet.py | 10 +++--- 7 files changed, 54 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6736bff..471ab4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "manifoldbt" -version = "0.12.2" +version = "0.12.3" description = "Rust-powered backtesting engine for quantitative research" requires-python = ">=3.9" license = { file = "LICENSE" } diff --git a/python/manifoldbt/plot/_decimate.py b/python/manifoldbt/plot/_decimate.py index be09163..63b788f 100644 --- a/python/manifoldbt/plot/_decimate.py +++ b/python/manifoldbt/plot/_decimate.py @@ -1,4 +1,4 @@ -"""Min/max time-series decimation for plotting — pure numpy. +"""Min/max time-series decimation for plotting - pure numpy. A chart is ~1000-2500 px wide, so plotting 10^5-10^6 samples draws hundreds of sub-pixel points per column and bloats saved HTML. Per pixel column we keep the diff --git a/python/manifoldbt/plot/_theme.py b/python/manifoldbt/plot/_theme.py index b7fd7c4..3aac784 100644 --- a/python/manifoldbt/plot/_theme.py +++ b/python/manifoldbt/plot/_theme.py @@ -1,16 +1,16 @@ -"""Clean dark theme — modern, readable, quant-oriented (plotly template).""" +"""Clean dark theme - modern, readable, quant-oriented (plotly template).""" from __future__ import annotations from contextlib import contextmanager # --------------------------------------------------------------------------- -# Color palette — neutral dark, no decorative colors +# Color palette - neutral dark, no decorative colors # --------------------------------------------------------------------------- WHITE = "#e8e6e3" GRAY = "#8a8a8a" DARK_GRAY = "#555555" -ACCENT = "#60a5fa" # Neutral blue — primary data line -ACCENT_ALT = "#a78bfa" # Subtle purple — secondary series +ACCENT = "#60a5fa" # Neutral blue - primary data line +ACCENT_ALT = "#a78bfa" # Subtle purple - secondary series GREEN = "#22c55e" # Positive only RED = "#ef4444" # Negative only ORANGE = "#f59e0b" # OOS / warning @@ -27,7 +27,7 @@ FONT_FAMILY = "Inter, system-ui, Segoe UI, Arial, sans-serif" MONO_FAMILY = "SF Mono, Fira Code, Cascadia Code, Consolas, monospace" # --------------------------------------------------------------------------- -# Colorscales (plotly format) — same stops as the old matplotlib colormaps +# Colorscales (plotly format) - same stops as the old matplotlib colormaps # --------------------------------------------------------------------------- CS_DIVERGING = [[0.0, "#b91c1c"], [0.5, "#262626"], [1.0, "#15803d"]] CS_SEQUENTIAL = [[0.0, "#b91c1c"], [0.5, "#d97706"], [1.0, "#15803d"]] diff --git a/python/manifoldbt/plot/_utils.py b/python/manifoldbt/plot/_utils.py index 6cd098f..27354da 100644 --- a/python/manifoldbt/plot/_utils.py +++ b/python/manifoldbt/plot/_utils.py @@ -67,7 +67,7 @@ def finalize( raise RuntimeError( f"Static image export to {ext} is optional and needs kaleido. " "Install it with: pip install manifoldbt[png] " - "(the default is the interactive chart — save to .html)" + "(the default is the interactive chart - save to .html)" ) from exc else: write_responsive_html(fig, path) diff --git a/python/manifoldbt/plot/_window.py b/python/manifoldbt/plot/_window.py index f70dcbd..fd1c64a 100644 --- a/python/manifoldbt/plot/_window.py +++ b/python/manifoldbt/plot/_window.py @@ -124,7 +124,7 @@ def show() -> None: _pending.clear() try: - import webview # noqa: F401 — only to detect the backend + import webview # noqa: F401 - only to detect the backend except ImportError: for div, title, _ in pending: _open_browser(div, title) diff --git a/python/manifoldbt/plot/research.py b/python/manifoldbt/plot/research.py index 4d4dda6..3d98131 100644 --- a/python/manifoldbt/plot/research.py +++ b/python/manifoldbt/plot/research.py @@ -1,4 +1,4 @@ -"""Charts for research analysis results (sweep, walk-forward, stability) — plotly.""" +"""Charts for research analysis results (sweep, walk-forward, stability) - plotly.""" from __future__ import annotations from pathlib import Path @@ -47,17 +47,46 @@ def _grid_window_size(nx: int, ny: int, plot: int = 720, cbar: int = 160, return (int(pw + cbar), int(ph + top)) -def _plateau_best(grid: np.ndarray): - """Plateau-optimal cell: Gaussian blur finds the center of the best stable - region, not a lucky spike (overfit-resistant). sigma = ~5% of each axis.""" - from scipy.ndimage import gaussian_filter +def _moving_average_1d(a: np.ndarray, radius: int, axis: int) -> np.ndarray: + """Edge-replicated moving average of window 2*radius+1 along axis (numpy).""" + if radius < 1: + return a + pad = [(radius, radius) if ax == axis else (0, 0) for ax in range(a.ndim)] + padded = np.pad(a, pad, mode="edge") + cumsum = np.cumsum(padded, axis=axis) + zero = np.zeros_like(np.take(cumsum, [0], axis=axis)) + cumsum = np.concatenate([zero, cumsum], axis=axis) + n = a.shape[axis] + width = 2 * radius + 1 + upper = np.take(cumsum, np.arange(width, width + n), axis=axis) + lower = np.take(cumsum, np.arange(0, n), axis=axis) + return (upper - lower) / width + +def _box_blur_2d(a: np.ndarray, sigma_y: float, sigma_x: float, passes: int = 3) -> np.ndarray: + """Separable box blur that approximates a Gaussian (central-limit theorem), + pure numpy. A scipy-free fallback for _plateau_best.""" + out = a.astype(float) + ry, rx = max(1, int(round(sigma_y))), max(1, int(round(sigma_x))) + for _ in range(passes): + out = _moving_average_1d(out, ry, axis=0) + out = _moving_average_1d(out, rx, axis=1) + return out + + +def _plateau_best(grid: np.ndarray): + """Plateau-optimal cell: a blur finds the center of the best stable region, + not a lucky spike (overfit-resistant). sigma = ~5% of each axis. Uses + scipy's Gaussian filter when installed, else a pure-numpy box blur so the + plotting extra needs no scipy.""" + filled = np.nan_to_num(grid, nan=np.nanmin(grid)) sigma_y = max(1.0, grid.shape[0] * 0.05) sigma_x = max(1.0, grid.shape[1] * 0.05) - smoothed = gaussian_filter( - np.nan_to_num(grid, nan=np.nanmin(grid)), - sigma=(sigma_y, sigma_x), - ) + try: + from scipy.ndimage import gaussian_filter + smoothed = gaussian_filter(filled, sigma=(sigma_y, sigma_x)) + except ImportError: + smoothed = _box_blur_2d(filled, sigma_y, sigma_x) return np.unravel_index(np.argmax(smoothed), smoothed.shape) @@ -137,7 +166,7 @@ def heatmap_2d( best_label = f"best: {best_val:{fmt}} ({x_param}={best_x:.0f}, {y_param}={best_y:.0f})" combos = nx * ny - main_title = title or f"{metric} — Parameter Sweep ({combos:,} combos)" + main_title = title or f"{metric} · Parameter Sweep ({combos:,} combos)" if best_label: main_title = f"{main_title}
{best_label}" fig.update_layout(title_text=main_title, hovermode="closest") @@ -224,7 +253,7 @@ def surface_3d( ) combos = len(x_vals) * len(y_vals) - main_title = title or f"{metric} — Surface ({combos:,} combos)" + main_title = title or f"{metric} · Surface ({combos:,} combos)" if best_label: main_title = f"{main_title}
{best_label}" diff --git a/python/manifoldbt/plot/tearsheet.py b/python/manifoldbt/plot/tearsheet.py index 9dc4799..18ec834 100644 --- a/python/manifoldbt/plot/tearsheet.py +++ b/python/manifoldbt/plot/tearsheet.py @@ -1,4 +1,4 @@ -"""Composite tearsheet — HTML strategy report with interactive plotly charts.""" +"""Composite tearsheet - HTML strategy report with interactive plotly charts.""" from __future__ import annotations import tempfile @@ -144,7 +144,7 @@ def tearsheet( dpi: int = 150, plotlyjs: str = "cdn", ) -> str: - """Strategy report — self-contained HTML page with interactive charts. + """Strategy report - self-contained HTML page with interactive charts. Returns the HTML string. Opens in browser when ``show=True``, writes to disk when ``save`` is given. @@ -225,7 +225,7 @@ def tearsheet( -{escape(strategy_name)} — Tearsheet +{escape(strategy_name)} · Tearsheet {plotly_js_tag} @@ -296,7 +296,7 @@ def research_report( save: Optional[Union[str, Path]] = None, dpi: int = 150, ) -> List[Any]: - """Research report — one figure per analysis (plotly Figures).""" + """Research report - one figure per analysis (plotly Figures).""" from manifoldbt.plot.research import ( heatmap_2d, stability, @@ -340,7 +340,7 @@ def research_report( def _fmt_hold_time(seconds): """Format holding time in human-readable units.""" if seconds <= 0: - return "—" + return "-" days = seconds / 86400 if days >= 365: return f"{days / 365:.1f}y"