Skip to content

Experiment Tracking — pmlab.tracking

Make every backtest auditable and reproducible: log the params, metrics, and gate decision of each run so a result is never a number you can't re-explain.

Two backends share one ExperimentTracker protocol:

Backend Dependency Use it for
LocalJSONTracker none (default) append-only JSON records on disk, zero setup
MLflowTracker pmlab[track] extra teams already on MLflow, richer UI

Basic Usage

from pmlab import LocalJSONTracker, rolling_origin_eval, compute_metrics, HoldoutGateResult

tracker = LocalJSONTracker(root="runs")

with tracker.run(experiment="weather_tmax", run_name="rf_baseline") as run:
    run.log_params({"model": "random_forest", "stride": 5, "taker_bps": 30.0})

    result = rolling_origin_eval(panel, model, stride=5, taker_bps=30.0)
    metrics = compute_metrics(result.trades)
    gate = HoldoutGateResult.evaluate(result.trades, required_segments=["all"])

    run.log_metrics(metrics)   # BacktestMetrics logs directly — no manual glue
    run.log_gate(gate)         # HoldoutGateResult too

log_metrics accepts a plain dict, a BacktestMetrics (any dataclass), or any object with a to_dict(). log_gate accepts a HoldoutGateResult. On exit the run is flushed with a finished status — or failed (plus the error) if the body raised, so partial runs are never silently lost.


Making a run re-runnable

run.log_reproducibility(record) stores a ReproducibilityRecord (seeds, library versions, resolved config, data fingerprint, metrics) in the run file, next to the params and metrics, so pmlab reproduce <run_id> --runs-dir runs can re-execute it. pmlab backtest --runs-dir runs does this for you. See reproducibility.md. Runs written before 0.10.0 have run.reproducibility == {}.


Reading runs back

runs = tracker.list_runs("weather_tmax")   # newest first, typed RunRecord
best = max(runs, key=lambda r: r.metrics["total_pnl"])
print(best.run_id, best.params, best.gate["decision"])

one = tracker.get_run("weather_tmax", best.run_id)   # point-in-time load

Each run is one immutable JSON file at runs/<experiment>/<run_id>.json. A new run never overwrites an old one (unique run_id), so the history is stable evidence — the same append-only, point-in-time philosophy as FeatureSnapshotStore.


MLflow backend (optional)

uv sync --extra track     # or: pip install pmlab[track]
from pmlab.tracking import MLflowTracker

tracker = MLflowTracker(tracking_uri="file:./mlruns")  # or a remote server
# same run(...) / log_params / log_metrics / log_gate / list_runs API

The module always imports safely; MLflow is only needed when you construct MLflowTracker. Without the extra, constructing one raises an actionable ImportError telling you to install pmlab[track].


API

Symbol Role
ExperimentTracker Protocol: run, list_runs, get_run
LocalJSONTracker Zero-dependency JSON backend (default)
MLflowTracker Optional MLflow backend (pmlab[track])
ActiveRun Live run handle: log_params, log_metrics, log_gate
RunRecord Immutable typed record of a completed run