# dpx_bt reference

`dpx_bt` runs intraday backtests on 1-minute / 5-minute bars of every stock that was in the NIFTY 50 at any time since
2016 (point-in-time membership, so no survivorship bias). Data snapshot: 2016-01-01 to 2026-08-31. Prices and volumes are
back-adjusted for splits and bonuses (by the data source), so multi-day lookbacks are consistent; a 2017 price is
the adjusted price, not the one printed on that day.

## Writing a strategy

A strategy is a function `strategy(p) -> Signals`. It is called once per calendar year with a `Panel` `p` that holds
that year's bars plus ~45 days of warm-up history. Build boolean DataFrames (rows = bar start times, columns = symbols)
and return them:

```python
import dpx_bt as bt

def strategy(p):
    orh, orl = p.opening_range(15)                 # 09:15-09:30 high / low, NaN until the range is complete
    long = p.once_per_day(p.crossed_above(p.close, orh))
    short = p.once_per_day(p.crossed_below(p.close, orl))
    return bt.Signals(long=long, short=short, stop=0.01, target=0.02)
```

### Execution rules (fixed; you cannot change them)
- A signal is read at the bar's CLOSE and filled at the NEXT bar's OPEN, same day only.
- Entries only inside the entry window (default 09:30-14:30 bar starts) and only for stocks that are NIFTY 50
  members that day.
- Every position is closed at the 15:15 bar's open (15:10 from 2026-08-03).
- One position per stock at a time; an opposite signal reverses the position.
- Fixed notional per trade (default Rs 5,00,000), whole shares.
- Costs: typical Indian discount-broker intraday charges (₹20 per order or 0.03% if lower, plus STT, NSE
  transaction, SEBI, stamp duty and GST) + 1 bp of slippage on every fill (entry and
  exit). Reports also show a +3 bps per fill stress test.

### `Signals` fields
| field | type | meaning |
|---|---|---|
| `long`, `short` | bool DataFrame | entry signals (evaluated at bar close) |
| `long_exit`, `short_exit` | bool DataFrame | optional exit signals (filled at the next bar's open) |
| `stop`, `target` | float or DataFrame | fraction of the fill price (0.01 = 1%). A DataFrame is read at the signal bar, e.g. `0.5 * p.atr() / p.close` |
| `trail` | bool | make the stop trailing |
| `entry_price` | DataFrame | fill ON the signal bar at this price (stop/limit orders at a known level, e.g. the OR high). The signal must then only use information known before that level trades |

## Panel `p`
Wide DataFrames, index = bar start time (IST), columns = symbols:
`p.open`, `p.high`, `p.low`, `p.close`, `p.volume`, `p.member` (bool: NIFTY 50 member that day and has a bar).

Per-bar Series: `p.day` (date), `p.tod` (minutes since midnight), `p.bar_of_day`, `p.first_bar`.
`p.at('10:00')`, `p.between('09:30', '11:00')` return bool Series; `p.bcast(series)` turns a per-bar Series into a
panel-shaped frame. `p.symbols`, `p.index`, `p.tf` ('1m'/'5m'), `p.minutes` (bar length).

Prior-day values (known before today's open): `p.prev_day('close'|'high'|'low'|'open'|'volume')`, `p.atr(14)`,
`p.daily_sma(n)`, `p.daily_ema(n)`. The full daily history is `p.daily.open/high/low/close/volume` (date x symbol;
row d contains all of day d: shift(1) before using it on day d) and `p.from_daily(df)` maps a daily frame onto bars.

Intraday values known at each bar's close: `p.day_open`, `p.day_high`, `p.day_low` (so far, including this bar),
`p.vwap`, `p.cum_volume`, `p.ret_since_open()`, `p.opening_range(minutes)` -> (high, low),
`p.rel_volume(minutes, lookback=14)` (first-`minutes` volume vs its average over prior days), `p.rsi(n)`,
`p.ema(n)`, `p.sma(n)`, `p.bar_atr(n)`.

Helpers: `p.crossed_above(a, b)`, `p.crossed_below(a, b)`, `p.once_per_day(mask)` (first True per stock per day).
Cross-sectional ranks work with pandas directly, e.g. `p.ret_since_open().rank(axis=1, ascending=False) <= 3`.

## Running
```python
report = bt.backtest(strategy, tf='5m', start='2016-01-01', end='2026-08-31', notional=500_000,
                     window=('09:30', '14:30'), symbols=None, name='my idea')
report.summary()   # dict: overall, periods (2016-20 / 2021-23 / 2024-26), years, warnings
report.markdown()  # the standard table
report.trades      # one row per trade: symbol, entry_time, exit_time, side, qty, entry_price, exit_price,
                   # gross_bps, net, net_3bps, notional
report.equity()    # cumulative net P&L by day
```
`5m` runs take ~10 s per year; `1m` runs ~5x longer. Use 5m unless the idea needs minute precision.

## Other data
- `bt.members()`: NIFTY 50 membership (symbol, instrument, from_date, to_date).
- `bt.daily_bars()`: daily OHLCV for the same stocks (columns ts, symbol, open, high, low, close, volume).
- `bt.intraday_bars(tf, start, end, symbols=None)`: long-format bars.
- `bt.reference(name)` with name one of `in.nse.index_daily` (all NSE indices incl. 'Nifty 50' and 'India VIX'),
  `in.nse.fo_futures_daily`, `in.nse.fo_participant_oi`, `in.nse.fo_participant_vol`, `in.nse.fii_deriv_stats`,
  `in.nsdl.fpi_daily` (all daily, column `date`).
- `bt.charges(buy_value, sell_value)`, `bt.net_pnl(long, qty, entry, exit, extra_bps=0)` for custom P&L.
