release: v0.13.1

This commit is contained in:
github-actions[bot]
2026-07-18 19:04:25 +00:00
parent 944c2f6d21
commit 1271cd17d8
4 changed files with 135 additions and 11 deletions
+38 -8
View File
@@ -13,6 +13,30 @@ Log de mesures : `gpu_funding_bench.md`. Toute nouvelle phase y ajoute sa sectio
---
## État au 2026-07-17 : TOUTES les phases 0-5 shippées en 0.13.0
Phases **0, 1a, 1b, 2, 3, 4, 5 LIVRÉES** (voir chaque section + `gpu_funding_bench.md`
Parts 0-17). La 0.13.0 est publiée sur PyPI. Il ne reste que le backlog Phase 6.
**Reste à faire (Phase 6), chaque cas se nomme quand il retombe sur CPU :**
- **Brackets multi-asset** : le kernel `sweep_t_multi` n'a pas de bracket (seul le
mono l'a, cf. Part 17). Raison nommée : « exit orders on a multi-asset universe
(the multi kernel has no bracket) ».
- **Frais per-venue** (2 exemples) : extension du fast path.
- **Cross-sectionnel** (1) : `sweep_t_multi`, restructuration en 2 passes.
- **MTF** (1) : grilles multiples, mapping d'index par timeframe.
- **Multi-asset + dérivées** (1) : l'exo/dérivée est globale, layout `[n_cols][n_sym][num_bars]`.
**Branches en attente (hors périmètre GPU, à trier) :**
- `feat/import-dataframe` (1 commit) : import de barres depuis un DataFrame en
mémoire, sans round-trip CSV. Feature réelle, jamais mergée.
- `fix/python-test-suite` (2 commits) : branche pytest en CI + aligne les tests
golden/sweep. Vaut le coup (la CI ne teste pas pytest aujourd'hui), MAIS
rendrait la CI rouge sur le `test_sweep_returns_one_result_per_combo` golden
préexistant tant qu'il n'est pas réglé (plancher de sortie Pro).
---
## Les trois couches qui bloquent une stratégie
1. **Ops transpileur manquants** : `Eq, Lag, Lead, RollingStd, RollingSum,
@@ -55,14 +79,14 @@ Sans données, l'ordre ci-dessous est notre meilleure estimation.
## Phase 1 — Quick wins périmètre (≈½ journée)
### 1a. Funding multi-asset dans `sweep_t_multi` (~15 lignes)
### 1a. Funding multi-asset dans `sweep_t_multi` (~15 lignes, **FAIT** `bb4396c`)
Le terme funding est déjà dans `sweep_t` (mono) : `capital += -position * close
* rate` par barre, par symbole ici. Retirer le garde-fou
`"multi-asset funding not yet on the GPU kernel"`.
- **Validation** : colonne funding synthétique injectée, e2e multi bit-identique
CPU==GPU (même harnais que le fix funding mono).
### 1b. Colonnes exogènes dans le sweep GPU (~30 lignes)
### 1b. Colonnes exogènes dans le sweep GPU (~30 lignes, **FAIT** `0a695e4`)
`load_exo_columns` ASOF-joint déjà l'exo sur `master_timestamps` → un
`Float64Array` de `num_bars`, indistinguable de `close` pour le kernel. Étendre
la résolution des `col_names` du transpileur aux colonnes exo alignées, retirer
@@ -179,13 +203,19 @@ plus intéressante du filtre), même sur CPU.
---
## Phase 4 SL/TP sur le fast path GPU (gros ; décision APRÈS Phase 0)
## Phase 4 : SL/TP sur le fast path GPU (**FAIT 2026-07-17**)
`orders.is_empty()` est probablement le garde-fou le plus coûteux en usage réel
(hypothèse à confirmer par les données de la Phase 0). Résolution intra-bar
SL/TP = high/low + sémantique de priorité déjà fixée à la 0.12.0 côté lite CPU.
Design séparé — ne pas commencer avant d'avoir les données de fréquence et le
merge des phases précédentes.
**Livré** : brackets (stop-loss / take-profit / trailing) simulés dans le kernel
CUDA `sweep_t`, transcription 1:1 des phases B1/B0 de `sim_fast_lite_core_single`
(la référence CPU, elle-même tenue à `run()` par `backtest_orders.rs`). Gate
`allow_exits` sur `fast_path_blocker` (mono-asset seulement) ; OHL packés en un
buffer (arité cudarc). **5.3x vs CPU à 24k combos**, GPU==CPU exact. Le trou le
plus gros de l'audit exemples (5/18) est bouché → **exemples 7→10/18 sur GPU**.
Deux bugs livrés trouvés en route : signe de `max_drawdown` inversé sur tout
chemin lite, et ~360 lignes de bracket lite inatteignables. Détail : Part 17.
Design d'origine (conservé) : résolution intra-bar SL/TP = high/low + sémantique
de priorité fixée à la 0.12.0 côté lite CPU.
## Phase 5 — Dispatch GPU/CPU (à faire À LA FIN)
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "manifoldbt"
version = "0.13.0"
version = "0.13.1"
description = "Rust-powered backtesting engine for quantitative research"
requires-python = ">=3.9"
license = { file = "LICENSE" }
+5 -2
View File
@@ -214,8 +214,11 @@ def tearsheet(
# ── plotly.js include ─────────────────────────────────────────
if plotlyjs == "inline":
import plotly.io as pio
plotly_js_tag = f"<script>{pio.get_plotlyjs()}</script>"
# get_plotlyjs lives in plotly.offline. plotly.io has never exposed it
# (checked on 5.24 and 6.9), so the old plotly.io lookup raised
# AttributeError on every inline report rather than embedding anything.
from plotly.offline import get_plotlyjs
plotly_js_tag = f"<script>{get_plotlyjs()}</script>"
else:
plotly_js_tag = '<script src="https://cdn.plot.ly/plotly-2.35.2.min.js" charset="utf-8"></script>'
+91
View File
@@ -0,0 +1,91 @@
"""Tearsheet rendering, in particular the offline (``plotlyjs="inline"``) report.
The inline path was written against ``plotly.io.get_plotlyjs``, which does not
exist and never has, so every offline report raised AttributeError. Nothing
exercised it, so nothing caught it. These tests render both modes for real.
"""
import os
import pytest
import manifoldbt as bt
from manifoldbt import run_with_parquet
pytest.importorskip("plotly")
tearsheet = pytest.importorskip("manifoldbt.plot.tearsheet").tearsheet
# Roughly the size of the embedded plotly runtime; the point is to tell an
# actually-embedded bundle from a one-line CDN script tag, not to pin a version.
_RUNTIME_BYTES = 2_000_000
_CDN_TAG = '<script src="https://cdn.plot.ly'
@pytest.fixture
def backtest_result(golden_buy_hold_dir):
strategy = bt.Strategy(
name="tearsheet_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_inline_report_embeds_the_runtime(backtest_result, tmp_path):
out = tmp_path / "inline.html"
tearsheet(backtest_result, save=str(out), show=False, plotlyjs="inline")
body = out.read_text(encoding="utf-8")
assert _CDN_TAG not in body, "inline report still loads plotly from the CDN"
assert out.stat().st_size > _RUNTIME_BYTES, (
f"inline report is only {out.stat().st_size} bytes, "
"the plotly runtime does not look embedded"
)
def test_cdn_report_links_the_runtime_instead_of_embedding_it(
backtest_result, tmp_path
):
out = tmp_path / "cdn.html"
tearsheet(backtest_result, save=str(out), show=False, plotlyjs="cdn")
body = out.read_text(encoding="utf-8")
assert _CDN_TAG in body
assert out.stat().st_size < _RUNTIME_BYTES
def test_get_plotlyjs_comes_from_plotly_offline():
"""Pin the import location that the inline path depends on."""
import plotly.io as pio
from plotly.offline import get_plotlyjs
assert not hasattr(pio, "get_plotlyjs"), (
"plotly.io grew a get_plotlyjs; the tearsheet import can be simplified"
)
assert len(get_plotlyjs()) > _RUNTIME_BYTES