# Artifacts (/scripting-v2/artifacts)



Python scripts often produce more than scalar metrics. Use `ctx.artifact_path(name)` to save charts, CSV files, model output, or reports in a directory owned by the current run.

```python
def on_finish(ctx, history):
    path = ctx.artifact_path("performance.png")
    plt.savefig(path, dpi=160, bbox_inches="tight")
    plt.close()

    return {"meta": {"chart": path}}
```

The method returns an absolute path as a string and creates missing parent directories:

```python
csv_path = ctx.artifact_path("tables/trades.csv")
frame.to_csv(csv_path, index=False)
```

## Storage location [#storage-location]

Artifacts are stored under:

```text
~/.market-lab/artifacts/<job-id>/
```

Live runs use the script job ID. Backtests use a generated run-specific directory so one backtest does not overwrite another.

On Unix systems, Market Lab creates each job artifact directory with owner-only `0700` permissions.

## Path safety [#path-safety]

The name must remain inside the job directory. Absolute paths and parent traversal are rejected:

```python
ctx.artifact_path("../outside.csv")  # rejected
ctx.artifact_path("/tmp/report.csv") # rejected
```

Use a relative name such as `report.csv` or `charts/equity.png`.

## Plot runtime PnL [#plot-runtime-pnl]

`ctx.pnl()` returns the PnL points recorded during the run. This makes the same runtime series available for post-analysis in `on_finish`:

```python
def on_finish(ctx, history):
    import matplotlib.pyplot as plt
    from datetime import datetime, timezone
    import matplotlib.dates as mdates

    points = ctx.pnl()
    if not points:
        return

    times = [
        datetime.fromtimestamp(point["t"] / 1000, tz=timezone.utc)
        for point in points
    ]
    pnl = [point["pnl"] for point in points]

    fig, ax = plt.subplots(figsize=(10, 5))
    ax.plot(times, pnl, linewidth=1.5, label="Net PnL")
    ax.axhline(0, color="gray", linewidth=1, linestyle="--")

    locator = mdates.AutoDateLocator()
    ax.xaxis.set_major_locator(locator)
    ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(locator))

    path = ctx.artifact_path("pnl.png")
    fig.savefig(path, dpi=160)
    print(f"Saved chart to: {path}")
    plt.close(fig)
```

Use `ctx.pnl(0)` for the latest point or `ctx.pnl(1)` for the previous point. An unavailable index returns `None`.

## Complete pandas and matplotlib example [#complete-pandas-and-matplotlib-example]

```python
import matplotlib.pyplot as plt
import pandas as pd

SOURCE = "btc@candles@binancef:timeframe=60"


def on_finish(ctx, history):
    frame = pd.DataFrame(history.source(SOURCE))
    if frame.empty:
        return

    frame["sma_20"] = frame["c"].rolling(20).mean()

    csv_path = ctx.artifact_path("tables/candles.csv")
    frame.to_csv(csv_path, index=False)

    axis = frame.plot(x="t", y=["c", "sma_20"], title="BTC close and SMA")
    axis.figure.savefig(
        ctx.artifact_path("charts/sma.png"),
        dpi=160,
        bbox_inches="tight",
    )
    plt.close(axis.figure)

    return {
        "meta": {
            "candles": csv_path,
            "chart": ctx.artifact_path("charts/sma.png"),
        }
    }
```

`on_finish` has a longer runtime limit than event hooks so final reporting can do more work. It still must complete within 60 seconds.

## Retention responsibility [#retention-responsibility]

Market Lab does not automatically delete artifact directories and does not impose an artifact disk quota. Monitor `~/.market-lab/artifacts` and remove old run directories according to your own retention policy.

`ctx.artifact_path` is a safe default, not a filesystem sandbox. Trusted Python code can still write elsewhere using normal Python APIs and the operating-system permissions of the daemon user.
