# Intraday and tick backtesting (/features/backtesting/intraday-and-tick-backtesting)

A bar only tells you its open, high, low, and close. For intraday strategies with tight stops, what
happened inside the bar decides the result. VBT runs intraday and tick backtesting in Python on bars
of any size, on irregular timestamps, and within trading sessions, and lets you resolve stops on
finer data than the signals use.

```python title="Resolve the same stops on hourly and on minute bars"
>>> minute_data = vbt.GBMOHLCData.pull(  # (1)
...     "SYN",
...     start="2024-01-01",
...     end="2024-02-01",
...     timeframe="1min",
...     std=0.0001,
...     seed=42,
... )
>>> hourly_data = minute_data.resample("1h")
>>> hourly_entries = hourly_data.symbol_wrapper.fill(False)
>>> hourly_entries.iloc[::4] = True  # (2)

>>> minute_entries = minute_data.symbol_wrapper.fill(False)
>>> entry_minutes = hourly_entries.index[hourly_entries] + pd.Timedelta(minutes=59)
>>> minute_entries[entry_minutes] = True  # (3)

>>> kwargs = dict(sl_stop=0.002, tp_stop=0.002)
>>> hourly_pf = vbt.PF.from_signals(hourly_data, hourly_entries, **kwargs)
>>> minute_pf = vbt.PF.from_signals(minute_data, minute_entries, **kwargs)

>>> def summary(pf):
...     stop_types = pf.orders.readable["Stop Type"].value_counts()
...     return pd.Series({
...         "trades": pf.trades.count(),
...         "stop losses": stop_types.get("SL", 0),
...         "take profits": stop_types.get("TP", 0),
...         "win rate": round(pf.trades.win_rate, 3),
...         "total return": round(pf.total_return, 3),
...     })

>>> pd.DataFrame({"hourly bars": summary(hourly_pf), "minute bars": summary(minute_pf)})
              hourly bars  minute bars
trades            186.000      186.000
stop losses       134.000      102.000
take profits       52.000       84.000
win rate            0.280        0.452
total return       -0.152       -0.036
```

1.  A month of seeded synthetic minute bars with a typical hourly range of about 0.8%.
2.  Enter at the close of every fourth hour.
3.  The same entries on the minute data: at the close of the last minute of each entry hour.

Both runs take the same 186 trades with the same 0.2% stop loss and take profit. On hourly bars,
many bars reach both levels, and with no way to know which came first the simulator assumes the stop
loss. Minute bars resolve more of those cases, and the win rate moves from 28% to 45%, much closer
to the 50% that symmetric stops on a random walk should produce.

## Ticks and irregular bars \[#ticks-and-irregular-bars]

Nothing in the engine assumes a fixed bar size. Tick prices go in as `close`, each row is processed
as its own moment, and the open, high, and low are not needed. Volume bars, range bars, dollar bars,
and trade-count bars built elsewhere work the same way, because the index only needs to be sorted:

```python title="Trade on ticks with irregular timestamps"
>>> index = pd.date_range("2024-01-01 09:30", periods=8, freq="1s").vbt.randomize(
...     step="100ms",
...     seed=42,
... )
>>> prices = [100.0, 100.2, 100.1, 100.4, 100.3, 100.6, 100.5, 100.7]
>>> ticks = pd.Series(prices, index=index)
>>> ticks.index.to_series().diff().dt.total_seconds().tolist()
[nan, 0.8, 1.6, 1.2, 0.8, 0.6, 1.0, 1.0]

>>> entries = pd.Series([True] + [False] * 7, index=index)
>>> exits = pd.Series([False] * 6 + [True, False], index=index)
>>> pf = vbt.PF.from_signals(ticks, entries, exits)
>>> pf.orders.readable[["Fill Index", "Side", "Price"]]
           Fill Index  Side  Price
0 2024-01-01 09:30:00   Buy  100.0
1 2024-01-01 09:30:06  Sell  100.5
```

Metrics that annualize, such as the Sharpe ratio, need a frequency, which an irregular index does
not have. Pass `freq` explicitly, use `freq="index_mean"` to take the average spacing of the
timestamps, or resample the portfolio's returns to a regular grid for analysis. Durations are
counted as bars times this frequency, so with the average spacing they are estimates. How long a
year is matters as well, as explained under
[Returns](/features/analytics/performance-metrics/#returns).

To build synthetic tick data for tests, `vbt.RandomOHLCData` and `vbt.GBMOHLCData` generate prices,
and `.vbt.randomize()` turns a regular index into irregular timestamps as above.

!!! warning "Windows on ticks count rows, not time"
    Most indicators take a window in rows. On ticks, a 20-row window can span a second or an hour
    depending on activity. Compute indicators on time bars, or use time-based windows such as
    `ticks.rolling("5min")`, and align the result back to the tick index.

## Inside the bar \[#inside-the-bar]

There are three ways to see inside a bar. The first is the one above: compute signals on the bars
you trade, and run the simulation on finer bars so stops and limits are checked at finer resolution.
Results can be resampled back to the original timeframe for reporting.

An hourly strategy can therefore use minute or tick prices for execution without changing its
indicator windows to minutes or ticks. VBT's alignment tools let you place an hourly closing signal
where that hour ends and make a daily trend filter available only after the day closes. See
[Multi-timeframe analysis](/features/data/multi-timeframe-analysis/) for combining signals, filters,
and prices from different timeframes.

The second is to place several orders inside one bar. The signal simulator processes at most one
order per column and bar. A flexible order function returns as many orders as you need, for example
a buy at the open and a sell at the close of the same bar, as shown on the
[Event-driven backtesting](/features/backtesting/event-driven-backtesting/#order-functions) page.

The third keeps the signal simulator and splits each bar into sub-bars: one for the open, one for
the range in between, and one for the close. Stops then fill in the first two, your own signals
execute in the last, and a position can close and reopen within one original bar. `pf.resample` maps
the result back to the original index.

!!! info "Cookbook"
    The members-only
    [multiple actions per bar](https://members.vectorbt.pro/cookbook/portfolio/#perform-multiple-actions-per-bar)
    recipe builds the three sub-bars and resamples the portfolio back.

!!! warning "What a bar cannot tell"
    Only the open and the close of a bar have a known order in time. When a strategy depends on
    whether the high came before the low, either use finer data or accept the pessimistic assumption.
    See [Realistic stop fills](/features/backtesting/stop-loss-and-take-profit/#realistic-stop-fills)
    for the exact rules.

## Trading sessions \[#trading-sessions]

A session is a mask over the index. Restrict entries to the session, add an exit on the last bar of
each session, and the strategy is flat every night:

```python title="Trade a New York session and be flat at every close"
>>> data = minute_data.loc["2024-01-08":"2024-01-12"]
>>> local_index = data.index.tz_convert("America/New_York")
>>> local_time = local_index.time
>>> in_session = (local_time >= pd.Timestamp("09:30").time()) & (
...     local_time < pd.Timestamp("16:00").time()
... )
>>> session_end = local_time == pd.Timestamp("15:59").time()

>>> sma = data.close.rolling(30).mean()
>>> entries = data.close.vbt.crossed_above(sma) & in_session & ~session_end
>>> exits = data.close.vbt.crossed_below(sma) | session_end
>>> pf = vbt.PF.from_signals(data, entries, exits)

>>> print(pf.trades.count())
91
>>> pf.assets.groupby(local_index.date).last().tolist()  # (1)
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
```

1.  The position at the end of each local day.

Rows without a price place no order. With `price="nextopen"`, a Friday signal whose next row is an
empty weekend row never fills. Drop the rows where the market is closed, for example with
`missing_index="drop"` when pulling several symbols, so the next row is the next session's open.

Signals that appear while the market is closed need a rule. Either drop them, or carry them to the
first bar of the next session so the live position matches the backtest. Rules such as "no exit on
the day of entry" are masks as well, comparing each exit with the date of the entry before it.

To build bars that only cover the session, for example a daily bar from 09:30 to 16:00 out of
24-hour data, filter the data to the session first or resample between custom bounds.

You can also test sessions independently. Put equal-length sessions in separate columns to compare a
strategy across days, with a fresh cash balance for each session. This is useful for strategies that
close every day and for finding whether a few sessions account for most of the result.

!!! info "Tutorial"
    The members-only [MTF analysis](https://members.vectorbt.pro/tutorials/mtf-analysis/aggregation/) tutorial
    aggregates intraday data into session bars with custom bounds.

## Longer intraday histories \[#longer-intraday-histories]

Simulate on ticks, then zoom out to the bigger picture. Resample the finished portfolio to minute,
hourly, or daily bars for analysis. VBT maps its order records to the new index and reconstructs
portfolio values and returns at that frequency, so you can inspect the broader result with less data
to process and plot. Keep the original portfolio for inspecting individual fills.

For a history processed in successive batches, a portfolio can also continue on the next batch with
its cash, positions, and stops carried forward. The same mechanism used for
[Live simulation](/features/backtesting/live-simulation/) lets a historical run cross batch
boundaries without closing positions at each boundary.


## Related pages

*   [Realistic backtests](/features/backtesting/backtest-realism/): Avoid look-ahead bias, impossible fills, and survivorship bias in your backtests
*   [Backtesting engine](/features/backtesting/backtesting-engine/): Simulate orders, signals, and callbacks across many assets and parameters at once
*   [Multi-timeframe analysis](/features/data/multi-timeframe-analysis/): Resample and realign data across timeframes by when each value became available
*   [Orders and execution](/features/backtesting/orders-and-execution/): Simulate limit and stop-entry orders, fill prices, timing, rejections, and real fills