mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 14:38:04 +00:00
release: v0.13.1
This commit is contained in:
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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>'
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user