mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 22:48:05 +00:00
release: v0.12.3
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "manifoldbt"
|
name = "manifoldbt"
|
||||||
version = "0.12.2"
|
version = "0.12.3"
|
||||||
description = "Rust-powered backtesting engine for quantitative research"
|
description = "Rust-powered backtesting engine for quantitative research"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
|
|||||||
@@ -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
|
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
|
sub-pixel points per column and bloats saved HTML. Per pixel column we keep the
|
||||||
|
|||||||
@@ -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 __future__ import annotations
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Color palette — neutral dark, no decorative colors
|
# Color palette - neutral dark, no decorative colors
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
WHITE = "#e8e6e3"
|
WHITE = "#e8e6e3"
|
||||||
GRAY = "#8a8a8a"
|
GRAY = "#8a8a8a"
|
||||||
DARK_GRAY = "#555555"
|
DARK_GRAY = "#555555"
|
||||||
ACCENT = "#60a5fa" # Neutral blue — primary data line
|
ACCENT = "#60a5fa" # Neutral blue - primary data line
|
||||||
ACCENT_ALT = "#a78bfa" # Subtle purple — secondary series
|
ACCENT_ALT = "#a78bfa" # Subtle purple - secondary series
|
||||||
GREEN = "#22c55e" # Positive only
|
GREEN = "#22c55e" # Positive only
|
||||||
RED = "#ef4444" # Negative only
|
RED = "#ef4444" # Negative only
|
||||||
ORANGE = "#f59e0b" # OOS / warning
|
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"
|
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_DIVERGING = [[0.0, "#b91c1c"], [0.5, "#262626"], [1.0, "#15803d"]]
|
||||||
CS_SEQUENTIAL = [[0.0, "#b91c1c"], [0.5, "#d97706"], [1.0, "#15803d"]]
|
CS_SEQUENTIAL = [[0.0, "#b91c1c"], [0.5, "#d97706"], [1.0, "#15803d"]]
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def finalize(
|
|||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Static image export to {ext} is optional and needs kaleido. "
|
f"Static image export to {ext} is optional and needs kaleido. "
|
||||||
"Install it with: pip install manifoldbt[png] "
|
"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
|
) from exc
|
||||||
else:
|
else:
|
||||||
write_responsive_html(fig, path)
|
write_responsive_html(fig, path)
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ def show() -> None:
|
|||||||
_pending.clear()
|
_pending.clear()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import webview # noqa: F401 — only to detect the backend
|
import webview # noqa: F401 - only to detect the backend
|
||||||
except ImportError:
|
except ImportError:
|
||||||
for div, title, _ in pending:
|
for div, title, _ in pending:
|
||||||
_open_browser(div, title)
|
_open_browser(div, title)
|
||||||
|
|||||||
@@ -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 __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
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))
|
return (int(pw + cbar), int(ph + top))
|
||||||
|
|
||||||
|
|
||||||
def _plateau_best(grid: np.ndarray):
|
def _moving_average_1d(a: np.ndarray, radius: int, axis: int) -> np.ndarray:
|
||||||
"""Plateau-optimal cell: Gaussian blur finds the center of the best stable
|
"""Edge-replicated moving average of window 2*radius+1 along axis (numpy)."""
|
||||||
region, not a lucky spike (overfit-resistant). sigma = ~5% of each axis."""
|
if radius < 1:
|
||||||
from scipy.ndimage import gaussian_filter
|
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_y = max(1.0, grid.shape[0] * 0.05)
|
||||||
sigma_x = max(1.0, grid.shape[1] * 0.05)
|
sigma_x = max(1.0, grid.shape[1] * 0.05)
|
||||||
smoothed = gaussian_filter(
|
try:
|
||||||
np.nan_to_num(grid, nan=np.nanmin(grid)),
|
from scipy.ndimage import gaussian_filter
|
||||||
sigma=(sigma_y, sigma_x),
|
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)
|
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})"
|
best_label = f"best: {best_val:{fmt}} ({x_param}={best_x:.0f}, {y_param}={best_y:.0f})"
|
||||||
|
|
||||||
combos = nx * ny
|
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:
|
if best_label:
|
||||||
main_title = f"{main_title}<br><span style='font-size:11px;color:{GRAY}'>{best_label}</span>"
|
main_title = f"{main_title}<br><span style='font-size:11px;color:{GRAY}'>{best_label}</span>"
|
||||||
fig.update_layout(title_text=main_title, hovermode="closest")
|
fig.update_layout(title_text=main_title, hovermode="closest")
|
||||||
@@ -224,7 +253,7 @@ def surface_3d(
|
|||||||
)
|
)
|
||||||
|
|
||||||
combos = len(x_vals) * len(y_vals)
|
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:
|
if best_label:
|
||||||
main_title = f"{main_title}<br><span style='font-size:11px;color:{GRAY}'>{best_label}</span>"
|
main_title = f"{main_title}<br><span style='font-size:11px;color:{GRAY}'>{best_label}</span>"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
from __future__ import annotations
|
||||||
|
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -144,7 +144,7 @@ def tearsheet(
|
|||||||
dpi: int = 150,
|
dpi: int = 150,
|
||||||
plotlyjs: str = "cdn",
|
plotlyjs: str = "cdn",
|
||||||
) -> str:
|
) -> 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``,
|
Returns the HTML string. Opens in browser when ``show=True``,
|
||||||
writes to disk when ``save`` is given.
|
writes to disk when ``save`` is given.
|
||||||
@@ -225,7 +225,7 @@ def tearsheet(
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{escape(strategy_name)} — Tearsheet</title>
|
<title>{escape(strategy_name)} · Tearsheet</title>
|
||||||
<style>{_CSS}</style>
|
<style>{_CSS}</style>
|
||||||
{plotly_js_tag}
|
{plotly_js_tag}
|
||||||
</head>
|
</head>
|
||||||
@@ -296,7 +296,7 @@ def research_report(
|
|||||||
save: Optional[Union[str, Path]] = None,
|
save: Optional[Union[str, Path]] = None,
|
||||||
dpi: int = 150,
|
dpi: int = 150,
|
||||||
) -> List[Any]:
|
) -> List[Any]:
|
||||||
"""Research report — one figure per analysis (plotly Figures)."""
|
"""Research report - one figure per analysis (plotly Figures)."""
|
||||||
from manifoldbt.plot.research import (
|
from manifoldbt.plot.research import (
|
||||||
heatmap_2d,
|
heatmap_2d,
|
||||||
stability,
|
stability,
|
||||||
@@ -340,7 +340,7 @@ def research_report(
|
|||||||
def _fmt_hold_time(seconds):
|
def _fmt_hold_time(seconds):
|
||||||
"""Format holding time in human-readable units."""
|
"""Format holding time in human-readable units."""
|
||||||
if seconds <= 0:
|
if seconds <= 0:
|
||||||
return "—"
|
return "-"
|
||||||
days = seconds / 86400
|
days = seconds / 86400
|
||||||
if days >= 365:
|
if days >= 365:
|
||||||
return f"{days / 365:.1f}y"
|
return f"{days / 365:.1f}y"
|
||||||
|
|||||||
Reference in New Issue
Block a user