From c36b5ec4ce663b79784f78e8203f575435bd88d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 18 Jul 2026 19:41:35 +0000 Subject: [PATCH] release: v0.13.2 --- pyproject.toml | 2 +- python/manifoldbt/plot/backtest.py | 9 ++- python/tests/test_plot_charts.py | 96 ++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 python/tests/test_plot_charts.py diff --git a/pyproject.toml b/pyproject.toml index 0940b89..98a6803 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "manifoldbt" -version = "0.13.1" +version = "0.13.2" description = "Rust-powered backtesting engine for quantitative research" requires-python = ">=3.9" license = { file = "LICENSE" } diff --git a/python/manifoldbt/plot/backtest.py b/python/manifoldbt/plot/backtest.py index b5be3e6..052c55f 100644 --- a/python/manifoldbt/plot/backtest.py +++ b/python/manifoldbt/plot/backtest.py @@ -420,7 +420,10 @@ def monthly_returns( colorbar=dict(ticksuffix="%", outlinewidth=0, thickness=12), hoverongaps=False, )) - fig.update_yaxes(autorange="reversed") + # The year labels are strings, but without an explicit type plotly + # reads them as numbers and interpolates: a single-year backtest drew + # ticks at 2,022.6 / 2,022.8 / 2023 / 2,023.2 instead of one "2023" row. + fig.update_yaxes(type="category", autorange="reversed") fig.update_xaxes(side="bottom", showspikes=False) fig.update_yaxes(showspikes=False) fig.update_layout(hovermode="closest") @@ -501,6 +504,10 @@ def returns_histogram( x=centers, y=counts, width=bw, marker_color=colors, opacity=0.7, marker_line_width=0, hovertemplate="%{x:.2%}: %{y}", + # Kept out of the legend: the bars are green or red by sign, so a + # single swatch misrepresents them, and unnamed it showed up as + # "trace 0". The legend exists for the Normal overlay only. + showlegend=False, )) fig.add_vline(x=0, line_color=DARK_GRAY, line_width=0.8, line_dash="dash") diff --git a/python/tests/test_plot_charts.py b/python/tests/test_plot_charts.py new file mode 100644 index 0000000..92d84fb --- /dev/null +++ b/python/tests/test_plot_charts.py @@ -0,0 +1,96 @@ +"""Chart-level regressions found by rendering a tearsheet and looking at it. + +Both defects below were invisible to file size, tag presence and trace counts. +They only showed up on screen, so they are pinned here at the figure-spec +level, which is cheap enough to run in CI without a browser. +""" +import os + +import pytest + +import manifoldbt as bt +from manifoldbt import run_with_parquet + +pytest.importorskip("plotly") + +backtest_plots = pytest.importorskip("manifoldbt.plot.backtest") + + +@pytest.fixture +def backtest_result(golden_buy_hold_dir): + strategy = bt.Strategy( + name="chart_probe", + signals={"signal": bt.lit(1.0)}, + position_sizing=bt.col("signal"), + ) + config = bt.BacktestConfig( + universe=[1], + time_range_start=0, + time_range_end=4_000_000_000, + bar_interval={"Days": 1}, + initial_capital=1000.0, + currency="USD", + execution=bt.ExecutionConfig( + signal_delay=1, + execution_price="AtClose", + max_position_pct=1.0, + allow_short=False, + allow_fractional=True, + skip_gap_bars=False, + position_sizing_mode="Units", + ), + fees=bt.FeeConfig(), + slippage={"FixedBps": {"bps": 0.0}}, + data_version="golden_v1", + rng_seed=7, + ) + return run_with_parquet( + strategy.to_json(), + config.to_json(), + os.path.join(golden_buy_hold_dir, "bars_1m.parquet"), + "golden_v1", + ) + + +def test_monthly_returns_year_axis_is_categorical(backtest_result): + """Year rows are labels, not a number line. + + The labels are strings already, but with no explicit axis type plotly + infers a linear scale and interpolates between them: a single-year + backtest rendered ticks at 2,022.6 / 2,022.8 / 2023 / 2,023.2 / 2,023.4. + """ + fig = backtest_plots.monthly_returns(backtest_result) + assert fig.layout.yaxis.type == "category" + + +def test_returns_histogram_has_no_unnamed_legend_entry(monkeypatch): + """No trace may reach the legend without a name. + + The histogram bars carried no name, so plotly labelled them "trace 0" and + gave them a single colour swatch even though the bars are green or red by + sign. The legend is there for the Normal overlay only. + + Returns are injected rather than backtested: the golden fixture spans less + than two UTC days, so ``daily_returns_array`` comes back empty and the + chart short-circuits before building any trace. Asserting over that empty + figure passes no matter what the code does. + """ + np = pytest.importorskip("numpy") + rng = np.random.default_rng(7) + monkeypatch.setattr( + backtest_plots, "daily_returns_array", lambda _result: rng.normal(0, 0.01, 500) + ) + + fig = backtest_plots.returns_histogram(object()) + + # Guard against the vacuous version of this test. + assert len(fig.data) >= 2, "expected the bars plus the Normal overlay" + + legend_names = { + trace.name for trace in fig.data if trace.showlegend is not False + } + assert legend_names, "no trace reaches the legend, the check would be vacuous" + assert None not in legend_names, "an unnamed trace renders as 'trace 0'" + assert not any( + (name or "").startswith("trace ") for name in legend_names + ), f"auto-generated trace label in the legend: {legend_names}"