mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 22:48:05 +00:00
release: v0.13.2
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "manifoldbt"
|
name = "manifoldbt"
|
||||||
version = "0.13.1"
|
version = "0.13.2"
|
||||||
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" }
|
||||||
|
|||||||
@@ -420,7 +420,10 @@ def monthly_returns(
|
|||||||
colorbar=dict(ticksuffix="%", outlinewidth=0, thickness=12),
|
colorbar=dict(ticksuffix="%", outlinewidth=0, thickness=12),
|
||||||
hoverongaps=False,
|
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_xaxes(side="bottom", showspikes=False)
|
||||||
fig.update_yaxes(showspikes=False)
|
fig.update_yaxes(showspikes=False)
|
||||||
fig.update_layout(hovermode="closest")
|
fig.update_layout(hovermode="closest")
|
||||||
@@ -501,6 +504,10 @@ def returns_histogram(
|
|||||||
x=centers, y=counts, width=bw,
|
x=centers, y=counts, width=bw,
|
||||||
marker_color=colors, opacity=0.7, marker_line_width=0,
|
marker_color=colors, opacity=0.7, marker_line_width=0,
|
||||||
hovertemplate="%{x:.2%}: %{y}<extra></extra>",
|
hovertemplate="%{x:.2%}: %{y}<extra></extra>",
|
||||||
|
# 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")
|
fig.add_vline(x=0, line_color=DARK_GRAY, line_width=0.8, line_dash="dash")
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
Reference in New Issue
Block a user