Skip to content
Docs

dpx_bt reference

The library every research post is run with, and the one the Ask assistant writes code against. Fixed execution rules and real costs, so a result here means the same thing everywhere.

1

Write a strategy

A function that turns the panel into entry signals.

def strategy(p):
    orh, orl = p.opening_range(15)
    long = p.once_per_day(p.crossed_above(p.close, orh))
    return bt.Signals(long=long, stop=0.01, target=0.02)
2

Run the backtest

Ten years, every NIFTY 50 stock, real costs.

report = bt.backtest(strategy, tf='5m',
                     start='2016-01-01', end='2026-08-31')
3

Read the report

Net, +3 bps stress, three periods, warnings.

print(report.markdown())
report.summary()['warnings']
report.trades.head()

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 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.

HTTP API

The platform API behind the website. Ask is in beta and rate-limited; endpoints may change.

MethodPathWhat it does
POST/api/askStart 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/datasetsDataset catalogue for the current snapshot.
POST/api/waitlistEmail sign-up: { email, interest }.
GET/api/healthService 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:

Each research post also has a Markdown twin: add .md to its address, e.g. /research/in/how-we-backtest.md.