# Backtest Results

Load and analyze prior backtest runs from notebook scripts.

<a id="runner-run"></a>
## runner.run

```python
runner.run(strategy, data, date_range, params, *, timeframe=None, starting_equity=100000, position_mode="sequential", slippage=0)
```

Run a strategy across a date range with one parameter set. Works for .py and .rs.

Runs the same engine the analyzer uses, returns a full BacktestResults with all analytics. Call from any Python context: editor Run, notebook cell. Works on Rust TBBO strategies too: you analyze the trades in Python while the strategy logic stays in Rust.

Runs through the optimization worker thread (separate Python subprocess), so it doesn't block the chart, the editor pool, or each other when called from inside an editor Run.

### Parameters

- `strategy` (`str`): Path to a .py or .rs file (relative to workspace strategies/ folder, or absolute)
- `data` (`str`): Path to a .parquet file (relative to workspace data/ folder, or absolute)
- `date_range` (`(str, str)`): Inclusive ISO date tuple, e.g. ("2025-01-01", "2025-01-31")
- `params` (`dict`): Strategy parameter values. Names must match the strategy file declarations.

### Returns

BacktestResults

### Example

```python
from quant_charts import runner
r = runner.run(
    strategy='strategies/ma_cross.py',
    data='ES.parquet',
    date_range=('2025-01-01', '2025-01-31'),
    params={'fast': 10, 'slow': 30},
)
print(r.summary())
r.plot_equity()
import matplotlib.pyplot as plt; plt.show()
```

### Notes

- For .rs strategies the analyzer engine is invoked exactly as the chart-attached path; trade results are identical.
- Sharing _runLock with the analyzer: if the Analyzer is mid-optimize, runner.run() blocks until it finishes (a "runner: waiting for analyzer" log line is printed while waiting).
- Worked TBBO/Rust example: notebooks/built-in/multi_day.ipynb.

<a id="runner-optimize"></a>
## runner.optimize

```python
runner.optimize(strategy, data, date_range, grid, *, metric="sharpe", timeframe=None)
```

Run a parameter grid sweep across a date range. Returns SweepResults.

The analyzer's full grid sweep, callable from Python. Each grid axis must be uniformly spaced (use list(range(...)) or numpy.linspace). Works for .py and .rs strategies; Rust strategies are routed through the same worker pool the Analyzer uses.

### Parameters

- `strategy` (`str`): Path to a .py or .rs file
- `data` (`str`): Path to a .parquet data file
- `date_range` (`(str, str)`): Inclusive ISO date tuple
- `grid` (`dict[str, list]`): Param-name to uniformly-spaced value list
- `metric` (`str`, default `"sharpe"`): "sharpe", "profit_factor", "total_pnl", "win_rate", "avg_snr", "median_snr", "recovery_factor", "payoff_ratio"

### Returns

SweepResults

### Example

```python
from quant_charts import runner
sweep = runner.optimize(
    strategy='strategies/momentum.rs',
    data='ES_TBBO.parquet',
    date_range=('2025-01-01', '2025-01-31'),
    grid={
        'period': list(range(10, 31, 2)),
        'threshold': [0.3, 0.5, 0.7],
    },
    metric='sharpe',
)
print(sweep.results_df.head())
sweep.scatter_3d('period', 'threshold', 'sharpeRatio')
sweep.best().plot_equity()
```

### Notes

- Grid axes MUST be uniformly spaced. The engine sweeps min/max/step; arbitrary value lists are rejected.
- sweep.best() lazily fetches trades for the top-scoring combo from the in-memory trade store.

<a id="qc-run-qc-optimize-qc-wfa"></a>
## qc.run / qc.optimize / qc.wfa

```python
qc.run / qc.optimize / qc.wfa(strategy, data, date_range, params|grid, ...)
```

Run a fresh backtest, sweep, or WFA inline from a notebook. Returns a `BacktestResults` (run) or `SweepResults` (optimize / wfa).

Inline backtest entry points. No UI involvement. Same engine the Analyzer uses; results are bit-for-bit identical.

For sweeps, `grid` axes must be uniformly spaced (use `range(...)` or `numpy.linspace`). For WFA, pass `in_sample_days` and `out_of_sample_days` plus a grid.

### Returns

BacktestResults | SweepResults

### Example

```python
import quant_charts as qc

r = qc.run('strategies/foo.py',
           data='ES.parquet',
           date_range=('2026-04-15', '2026-04-15'),
           params={'fast': 9, 'slow': 21})
r.summary()

opt = qc.optimize('strategies/foo.py',
                  data='ES.parquet',
                  date_range=('2026-04-15', '2026-04-15'),
                  grid={'fast': range(5, 21), 'slow': range(15, 50)})
opt.top(10)
```

<a id="qc-runs-load_run-save_run-delete_run"></a>
## qc.runs / load_run / save_run / delete_run

```python
qc.runs / load_run / save_run / delete_run() | (name)
```

Saved-runs registry. List, load, save, or delete named exports.

Saved runs persist with an `index.json` registry. Names are alphanumeric plus `_-.`. Saved runs are explicit and never overwritten by future analyzer runs; call `delete_run(name)` to clean up.

### Returns

pd.DataFrame | BacktestResults | dict | None

### Example

```python
import quant_charts as qc

qc.runs()                       # DataFrame: name, strategy, dateRange, kind, nTrades, savedAt
r = qc.load_run('ema_cross_a')
qc.save_run('my_combo')         # persist whatever\'s in memory
qc.delete_run('my_combo')
```

<a id="qc-cancel"></a>
## qc.cancel

```python
qc.cancel()
```

Cancel the in-flight runner call. Safe from a signal handler or another thread.

Sends a cancel signal to the runner bridge. The currently blocked `qc.run` / `qc.optimize` / `qc.wfa` call raises `RunnerCancelled` on its next poll. No-op when no call is in flight.

Use case: Jupyter cell that started a long sweep; press the stop button (which triggers cancel) without waiting for the worker to finish naturally.

### Returns

None

### Example

```python
import quant_charts as qc
import threading

def stop_after(seconds):
    import time
    time.sleep(seconds)
    qc.cancel()

threading.Thread(target=stop_after, args=(60,), daemon=True).start()
# Will be cancelled after 60s if still running:
opt = qc.optimize('strategies/foo.py', data='ES.parquet',
                  date_range=('2026-04-01', '2026-04-30'),
                  grid={'fast': range(5, 30), 'slow': range(20, 60)})
```

<a id="load_csv"></a>
## load_csv

```python
load_csv(name_or_path?)
```

Load a CSV trade export into a BacktestResults for analysis.

Loads trades from a CSV file exported via the Analyzer's Export button. The CSV is parsed back into a full BacktestResults with equity curve, so all analysis methods (summary, plot_equity, monte_carlo, filtering) work the same as for an inline `qc.run()`.

With no argument, loads the most recent CSV in the exports/ folder.

### Parameters

- `name_or_path` (`str`, default `None`): CSV filename (e.g. `"my_backtest.csv"`), full path, or `None` for most recent

### Returns

BacktestResults

### Example

```python
from quant_charts.results import load_csv
r = load_csv()                     # most recent export
r = load_csv("MA_Cross_2024.csv")  # specific file
r.summary()
r.longs().plot_equity()
r.monte_carlo(1000).plot()
```

### Notes

- CSV files are saved to the exports/ folder when you click Export in the Analyzer top bar.
- Tags are preserved. `r.by_tag("morning")` works on CSV-loaded results.
- Use `list_exports()` to see available CSV files.

<a id="list_exports"></a>
## list_exports

```python
list_exports()
```

List available CSV trade exports with dates and sizes.

Prints a formatted table of all CSV files in the exports/ folder, sorted by modification time (newest first). Returns a list of filenames.

### Returns

list[str]

### Example

```python
from quant_charts.results import list_exports
files = list_exports()
```

### Output

```text
>>> list_exports()
Filename                                 Modified             Size
----------------------------------------------------------------------
MA_Cross_2024-01-01_to_2024-06-30.csv   2024-07-15 14:32       12.3K
RSI_Scalper_2024-03-01_to_2024-03-31.csv 2024-07-14 09:18        4.1K
```

<a id="r-summary"></a>
## r.summary

```python
r.summary()
```

Print comprehensive stats table.

Displays total trades, win rate, PnL, profit factor, Sharpe ratio, max drawdown, MAE/MFE, streaks, average duration, and more.

### Returns

None (prints table)

### Example

```python
r.summary()
```

<a id="r-trades"></a>
## r.trades

All trades as a pandas DataFrame.

Columns: id, side, entryTime, exitTime, entryPrice, exitPrice, pnl, pnlPercent, mae, mfe, snr, duration, exitReason, tags, stopLoss, takeProfit. Also has `entryTime_et` / `exitTime_et` (Eastern Time).

### Returns

pandas DataFrame

### Example

```python
# Standard pandas operations on trades
r.trades.groupby('side')['pnl'].describe()
r.trades[r.trades['duration'] < 60000]
r.trades.query('exitReason == "sl_hit"')['pnl'].hist()
```

<a id="r-winners-r-losers"></a>
## r.winners / r.losers

```python
r.winners / r.losers()
```

Filter by PnL sign. Returns a new BacktestResults.

All filter methods return a **new** BacktestResults. The original is never modified. The filtered result has its own rebuilt equity curve.

### Returns

BacktestResults

### Example

```python
r.winners().summary()
r.losers().plot_equity()
```

<a id="r-longs-r-shorts"></a>
## r.longs / r.shorts

```python
r.longs / r.shorts()
```

Filter by trade side.

### Returns

BacktestResults

### Example

```python
r.longs().winners().summary()  # winning longs
```

<a id="r-by_tag"></a>
## r.by_tag

```python
r.by_tag(tag_name)
```

Filter to trades that have a specific tag.

### Parameters

- `tag_name` (`str`, default `required`): Tag name to filter by

### Returns

BacktestResults

### Example

```python
r.by_tag("morning").summary()
```

<a id="r-where"></a>
## r.where

```python
r.where(**kwargs)
```

Django-style field lookups for flexible filtering.

Operators: `field=value` (exact), `field__gt` (>), `field__lt` (<), `field__gte` (>=), `field__lte` (<=), `field__ne` (!=), `field__in` (list), `field__contains` (string), `field__hour` (ET hour), `field__dayofweek` (0=Mon).

Three-part accessor for timestamps: `entryTime__hour__gte=9`

### Returns

BacktestResults

### Example

```python
# Morning longs that won
r.longs().where(
    entryTime__hour__gte=9,
    entryTime__hour__lte=11,
    pnl__gt=0
).summary()
```

<a id="r-plot_equity"></a>
## r.plot_equity

```python
r.plot_equity(by_trade?)
```

Plot the equity curve.

### Parameters

- `by_trade` (`bool`, default `True`): `True` = x-axis is trade number, `False` = time

### Returns

matplotlib Figure

### Example

```python
r.plot_equity()
r.winners().plot_equity()
```

<a id="r-monte_carlo"></a>
## r.monte_carlo

```python
r.monte_carlo(n?, method?)
```

Monte Carlo simulation for risk assessment.

Shuffles or bootstraps trade PnL to estimate range of outcomes. Returns a `MonteCarloResult` with `.plot()`, `.summary()`, `.percentile()`, `.ruin_probability()`, and `.confidence_interval()` methods.

### Parameters

- `n` (`int`, default `1000`): Number of simulations
- `method` (`str`, default `"bootstrap"`): `"bootstrap"` or `"shuffle"`

### Returns

MonteCarloResult

### Example

```python
mc = r.monte_carlo(1000)
mc.plot(ci=0.95)
mc.summary()
mc.ruin_probability(5000)
```

<a id="r-simulate_sl_tp"></a>
## r.simulate_sl_tp

```python
r.simulate_sl_tp(sl, tp)
```

Simulate different SL/TP using MAE/MFE data (instant, no re-run).

### Parameters

- `sl` (`float`, default `required`): Stop loss in dollars
- `tp` (`float`, default `required`): Take profit in dollars

### Returns

BacktestResults

### Example

```python
sim = r.simulate_sl_tp(sl=30, tp=15)
print(f"Original: ${r.trades.pnl.sum():.2f}")
print(f"Simulated: ${sim.trades.pnl.sum():.2f}")
```

<a id="r-monthly_returns"></a>
## r.monthly_returns

```python
r.monthly_returns()
```

Year x Month pivot table of PnL.

### Returns

pandas DataFrame

### Example

```python
r.monthly_returns()
```
