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:
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
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 ofin.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, columndate).bt.charges(buy_value, sell_value),bt.net_pnl(long, qty, entry, exit, extra_bps=0)for custom P&L.
HTTP API
The platform API behind the website. Ask is in beta and rate-limited; endpoints may change.
| Method | Path | What it does |
|---|---|---|
| POST | /api/ask | Start an Ask job: { question, follow_up_of? }. Returns { id }. |
| GET | /api/ask/{id} | Job status, progress events, final answer and the runs it made. |
| GET | /api/runs/{run_id} | One run: the code, the full report and the weekly equity curve. |
| GET | /api/datasets | Dataset catalogue for the current snapshot. |
| POST | /api/waitlist | Email sign-up: { email, interest }. |
| GET | /api/health | Service status, data snapshot and model. |
For AI agents and tools
Everything on DataPointX is available in plain formats for language models, MCP fetch tools and scripts:
Site map for AI assistants /llms-full.txt
All research, full text /docs/dpx_bt.md
This reference as Markdown /research/index.json
Every result as JSON
Each research post also has a Markdown twin: add .md to its address, e.g. /research/in/how-we-backtest.md.