Equity Research

Equity Research In Plain English

The Equity Research page is a simulation lab for Indian equity strategies. It asks:

  • Which stocks would this strategy have selected in the past?
  • What would the portfolio have bought, sold, and held?
  • How much return, drawdown, cost, tax, and idle cash would the run have produced?
  • Which strategy looks useful enough to study further?

It does not place orders. The research subsystem reads market data, runs a backtest, and writes results only to research_* tables. It never imports the live execution engine, never changes live trades, and never touches go-live state.

How a run works | Important concepts | Reading results | Data providers | Safety boundary

How A Run Works

Think of a backtest run as a replay of history with strict rules.

  1. Pick a strategy, such as momentum_quality.
  2. Pick a universe, such as nifty500.
  3. Pick a data provider, usually yfinance, broker, or dhan.
  4. Choose a date range, portfolio size, and rebalance frequency.
  5. On each rebalance date, the engine gives the strategy only the information available at that date.
  6. The strategy scores stocks and returns candidates.
  7. The engine buys the selected names, sells names that no longer qualify, applies costs and taxes, and marks the portfolio every day.
  8. The dashboard stores the run so you can inspect the equity curve, holdings, trades, signals, tax, risk, and logs.

The output is not a recommendation. It is evidence: what a configured rule would have done under the configured assumptions.

Important Concepts

Universe Mode

point_in_time is the safer research mode. It uses effective-dated universe membership, so a 2020 run does not accidentally use companies that only joined the index later.

current uses today's universe membership for the whole backtest. That is easier to run, but it can introduce survivorship bias because failed or removed names may be missing.

Rebalance Frequency

Rebalance frequency controls how often the strategy can change the portfolio.

  • monthly fits slower trend, quality, and value strategies.
  • weekly fits event and earnings-drift strategies.
  • daily fits fast mean-reversion and pair-trading strategies.

Costs, Taxes, And Idle Cash

The engine models transaction costs, FIFO tax lots, STCG/LTCG treatment, and idle cash. Idle cash can be swept into the configured liquid-fund assumption in the long-only engine. Results are shown at gross, net-of-costs, and net-of-tax levels.

SIP Allocation

Research runs can apply scheduled SIP contributions in either of two ways:

  • buy_only_current_targets immediately buys only positions that are underweight relative to the target portfolio established by the latest rebalance. It never sells to deploy a contribution, uses whole-share cost-inclusive sizing, and sends unusable residual cash to the liquid fund.
  • next_rebalance leaves the contribution in cash until the next scheduled strategy rebalance. This remains the default.

When a SIP date is also a scheduled rebalance date, the engine issues units and adds the contribution first, then performs one full rebalance. It does not run a second contribution-only allocation. Contributions issue units at the pre-flow net-of-tax NAV, so adding capital does not itself count as investment return.

SIP support is not universal. Pair-arbitrage and options-premium strategies cannot maintain investor units and are always unsupported; a sleeve combination is always unsupported; a composite/fallback/regime-adaptive/slot-fill combination is supported only when every one of its member strategies individually supports SIP. GET /api/research/capabilities reports this per strategy (strategy_contribution_support) and, when a combine_mode/combine_members selection is supplied, for that exact combined selection (combined_contribution_support) — the dashboard's New Backtest Run form reads this to grey out and explain an unsupported SIP toggle before submission, but the backend re-validates the same rule on every run creation regardless of what the UI shows.

Every return metric — CAGR, Sharpe, Sortino, max drawdown, Calmar, at each of the gross/net-of-costs/net-of-tax levels — is computed from a flow-safe daily performance index that starts at 10.0 and compounds only the day's actual market return, isolated from any same-day contribution. This is what keeps a contribution from ever showing up as investment return or resetting/reducing drawdown: the index (and the drawdown computed from its running peak) moves only with price, never with a deposit.

Dynamic Cash

Dynamic cash is a de-risking overlay. In weak benchmark regimes, it can reduce the equity sleeve and hold more cash. When enabled, position sizing becomes portfolio-value based, so the target per stock scales with the changing book instead of being fixed to the initial capital.

Reading Results

KPI Strip

Start with CAGR, XIRR, Sharpe, Sortino, max drawdown, turnover, tax, and cost. A strategy with high CAGR but extreme drawdown may still be unsuitable.

Equity Curve

Compare gross, net-of-costs, and net-of-tax curves. A large gap between gross and net means turnover or taxes are doing real damage.

For a public benchmark comparison that uses an exact 10-year fund window, see the Parag Parikh comparison page in the research docs.

Investor Results

For a contribution-bearing run, the Overview tab also shows a net-of-tax investor view: total invested, current corpus, investment gain and gain percentage, investor XIRR, NAV CAGR, latest NAV, units held, and contribution count. The NAV chart defaults to the net-of-tax NAV series and can optionally overlay the three flow-safe performance-index series for comparison; a separate chart plots net-of-tax corpus against cumulative contributions so growth from the market is never confused with growth from depositing more money. A Contributions tab lists every scheduled/effective contribution with its base and step-up amounts, exportable to CSV. Runs from before the analytical-index fix landed lack the performance_index_* fields entirely — the UI shows a historical-data notice on those runs' NAV chart rather than silently deriving a flow-safe number from raw NAV.

Holdings

The Holdings tab shows what the portfolio owned on a selected day. Use it to check whether the strategy is concentrated in one sector or holding names that do not match the thesis.

Trades

The Trades tab explains actual simulated buys and sells. For combined sleeve runs, trades include sleeve tags.

Signals

The Universe or Signals view shows pass/fail and score details, including names that were considered but not selected. This is where you debug why a stock did or did not enter.

Logs

Logs are useful when a provider had missing data, a universe was not loaded, or a run was cancelled or restarted.

Data Providers

Research supports these provider modes:

  • yfinance: default adjusted equity data path, useful for long historical studies.
  • dhan: direct read-only Dhan historical data path.
  • broker: active-broker research mode; currently delegates to read-only Dhan research when the active broker is Dhan.
  • fake: deterministic test provider.

Dhan historical OHLC is raw. Until a full corporate-action adjustment source is wired in, prefer yfinance for adjusted long-horizon equity backtests, or explicitly treat Dhan runs as raw-price studies.

Maintaining Research Data

Point-In-Time Universe

Refresh current index membership:

venv/bin/python -m arb_bot.research.cli.refresh_universe --all

Backfill older membership when you need cleaner historical tests:

make backfill-universe-wayback

Fundamentals

Refresh quarterly fundamentals:

venv/bin/python -m arb_bot.research.cli.refresh_fundamentals \
  --universe nifty500 \
  --provider yfinance

Events

Refresh earnings events:

venv/bin/python -m arb_bot.research.cli.refresh_events \
  --universe nifty500 \
  --provider yfinance

Safety Boundary

The research system is deliberately isolated from live trading:

  • It must not import arb_bot.bot, arb_bot.execution.engine, or arb_bot.command_handler.
  • It must not expose an order surface.
  • It must not call place_order.
  • It persists research data under research_* tables.
  • Tests in tests/research/test_import_isolation.py and tests/research/test_provider_safety.py enforce the boundary.

Deleting a run removes that run's research artifacts, exports, logs, trades, signals, holdings, tax lots, cash flows, and config rows. It does not delete shared universe membership or market-data cache rows, and it never touches live trades.

PDF Report structure

The generated Equity Research PDF renders six decision-quality sections in the order below. The renderer contract is fixed; the runner populates each section's underlying metric with an explicit placeholder when its acquisition pipeline is not yet wired so a section never silently disappears or fabricates data.

⚠️ Production scope today. Only the synthesis rating pipeline (section 1) is wired into runner.run() on this branch. Sections 2-6 are present in the renderer and rendered on every PDF, but the runner stamps deterministic None / placeholders for them — the corresponding sections display their documented "N/A" or "first report" fallback state. The peer / SOTP / transcript / fact-library / pinned-baseline acquisition work is a separate follow-up; once those pipelines land they replace the setdefault stamps in arb_bot/research/runner.py without changing the renderer's contract. Operators reading a generated PDF today should not expect those sections to carry live data.

The six sections, in order:

  1. Synthesis rating — shows the deterministic rating and rationale on the cover and in the report summary. When no contributor module populates the inputs, the renderer surfaces an explicit Synthesis unavailable placeholder instead of fabricating a HOLD default. Populated today by the synthesis module.
  2. Fact library references — renders key facts from the shared fact library so repeated claims use one source value. Placeholder today (empty {}); section is omitted from the PDF until the fact-library acquisition pipeline lands.
  3. Peer table — compares supplied peers by symbol, last close, one-year return, and rating. Always renders the section heading and a Peer Comparison — Data not collected by this run placeholder when the peer pipeline hasn't supplied data, so the document structure stays predictable for AI/LLM ingestion.
  4. SOTP stub — shows the target price and per-segment equity values, or the explicit SOTP — Data not collected by this run placeholder when segment EBITDA is missing. Always renders the section heading + placeholder so operators never see a fabricated table.
  5. Management quotes — includes up to five earnings-call quotes with speaker, role, transcript page, and date attribution. Placeholder today (None); the section is omitted from the PDF until the transcript pipeline lands.
  6. Prior-baseline diff — compares the current synthesis rating, headline performance metrics, and rationale with the prior pinned baseline; the first report instead prompts the operator to pin a baseline for future comparisons. Placeholder today (None); the section renders the "first report" prompt because no pinned baseline exists yet.