Skip to content

Exit analytics

How much of each move your client actually kept.

Entry quality gets all the attention, but most retail damage is done on the way out — a winner held past its peak until it round-trips, or cut three minutes before the move happened. This endpoint measures that directly: for every closed trade we replay the instrument's candles over the hold, find the best price it reached (MFE, maximum favourable excursion) and the worst (MAE, maximum adverse excursion), and compare both against what the client actually booked.

Nothing here is a forecast. It is arithmetic on what already happened, which makes it the easiest number in the SDK to show a client without an argument.

Requires the stats feature on your account.

GET /users/{user_ref}/exit-analytics

Query parameters

Shared by both endpoints on this page, and identical to the rest of the SDK's filter vocabulary.

Parameter Type Default Description
segment enum all EQ or FO.
trade_type enum all INTRADAY · BTST · STBT · SWING.
range enum 90d 7d · 30d · 90d · 1y · 3y · all.
tag string — Restrict to one behaviour, e.g. paper_hands. Tag ids match the pattern reference.

The three enums are validated, so a typo returns 422 rather than quietly widening the window — a mistyped range that fell back to the client's whole history would be worse than an error.

While the analysis is running

Excursions are computed in the same pass as the behavioural stats, so this endpoint uses the same contract as /stats — poll until status is ready:

{ "user_ref": "CLIENT00423", "status": "analysing", "stage": "rebuilding" }

Response

{
  "user_ref": "CLIENT00423",
  "status": "ready",
  "coverage": { "covered": 38, "total": 53, "scope": "intraday" },
  "one_fix": {
    "profile": "round_tripped",
    "headline": "You're giving your winners back",
    "detail": "89% of your leak is round-tripped — ₹1.09L of profit you were already up on, handed back before you exited. Your winners peak around 11 min in; you hold ~17.",
    "rule": "Trail a stop at 1× ATR once you're up half your average peak — or set a hard time-stop and let it exit for you.",
    "why": "Your problem is holding too long, not exiting too soon — tightening entries or targets won't touch this leak.",
    "stats": [
      { "k": "Capture rate",  "v": "27%",    "sub": "of the available move", "tone": "warn" },
      { "k": "Round-tripped", "v": "₹1.09L", "sub": "89% of your leak",      "tone": "bad" },
      { "k": "Cut early",     "v": "₹13k",   "sub": "11% of your leak",      "tone": "" },
      { "k": "Winners peak",  "v": "11 min", "sub": "you hold ~17 min",      "tone": "" }
    ]
  },
  "scorecard": {
    "actual": 23634.75,
    "best_exit": 132576.01,
    "capture_rate": 27.4,
    "hold_to_close_delta": -12489.03,
    "best_repeatable_alternative": {
      "dimension": "Duration", "segment": "Scalp (<15m)", "n": 19,
      "your_pnl": 14218.76, "best_alt": 90102.37, "opportunity": 75883.61
    }
  },
  "leak": { "round_tripped": 108941.26, "cut_early": 13390.22, "total": 122331.48 },
  "hold_longer_curve": [
    { "horizon": "exit",  "total_pnl": 23634.75, "n": 38 },
    { "horizon": "eod",   "total_pnl": 11145.72, "n": 38 },
    { "horizon": "eod+1", "total_pnl": 12448.36, "n": 30 },
    { "horizon": "eod+2", "total_pnl": -16122.87, "n": 30 },
    { "horizon": "eod+3", "total_pnl": 4742.19, "n": 30 }
  ],
  "hold_reliability": [
    { "horizon": "exit",  "win_rate": 61, "pct_improve": null },
    { "horizon": "eod",   "win_rate": 42, "pct_improve": 24 },
    { "horizon": "eod+1", "win_rate": 47, "pct_improve": 30 },
    { "horizon": "eod+2", "win_rate": 33, "pct_improve": 23 },
    { "horizon": "eod+3", "win_rate": 47, "pct_improve": 43 }
  ],
  "best_exit_clock": [
    { "hour": 9,  "actual": 2,  "optimal": 2 },
    { "hour": 10, "actual": 4,  "optimal": 5 },
    { "hour": 11, "actual": 5,  "optimal": 5 },
    { "hour": 12, "actual": 6,  "optimal": 5 },
    { "hour": 13, "actual": 5,  "optimal": 5 },
    { "hour": 14, "actual": 5,  "optimal": 6 },
    { "hour": 15, "actual": 11, "optimal": 7 }
  ],
  "biggest_leaks": [
    { "dimension": "Duration",    "segment": "Scalp (<15m)",   "n": 19, "your_pnl": 14218.76, "best_alt": 90102.37, "opportunity": 75883.61 },
    { "dimension": "Duration",    "segment": "Intraday (<1d)", "n": 19, "your_pnl": 9415.99,  "best_alt": 66964.91, "opportunity": 57548.92 },
    { "dimension": "Time of day", "segment": "Midday (11-14)", "n": 16, "your_pnl": 10297.10, "best_alt": 67360.48, "opportunity": 57063.38 }
  ]
}

In this client's book the exits are the problem: they are keeping 27% of the move they were already up on, and the hold_longer_curve says holding to the close would have been worse (₹23,635 → ₹11,146), so this is not a patience problem — it is a no-exit-rule problem. That is the distinction one_fix exists to make.

Fields

Field Type Description
status enum ready or analysing (poll again).
coverage object covered = trades we had candles for; total = trades matching the filters. Render every number below against covered, never against the client's whole tradebook. See Coverage.
empty bool Present and true when covered is 0 — no scorecard, no leak, nothing to show yet.
one_fix object \| null The single highest-value change, already written as prose: headline, detail, rule, why, plus 2–4 stats tiles (tone is "" / warn / bad). Render it verbatim if you want a zero-effort integration. profile is one of round_tripped, cut_early, mixed, or insufficient (fewer than 10 covered exits — the numbers are shown but no leak is named). null when there is no material leak to fix.
scorecard.actual float ₹ the client actually booked (gross, price-frame — matches the MFE frame, so it excludes charges).
scorecard.best_exit float ₹ had every trade been exited at its in-hold peak. Hindsight, and labelled as such — it is the denominator, not a target.
scorecard.capture_rate float The headline number. Mean per-trade realised ÷ peak, as a percentage. A disciplined book sits in the 35–55% band; below ~25% the exits are the problem, not the entries.
scorecard.hold_to_close_delta float ₹ difference had every position been held to that session's close instead. Negative means exiting early was right.
scorecard.best_repeatable_alternative object \| null The costliest row from biggest_leaks — the one dimension worth changing first.
leak.round_tripped float ₹ reached and then given back before the exit.
leak.cut_early float ₹ the same session went on to offer after the exit.
leak.total float The two added together.
hold_longer_curve[] array Total ₹ at each exit horizon: the actual exit, the session close (eod), and 1–3 sessions out. n differs per horizon — option legs have no eod+N (expiry), so compare horizons by shape, not by absolute ₹.
hold_reliability[] array Per horizon: win_rate and pct_improve — the share of trades that would have been better at that horizon. pct_improve is null for exit (it is the baseline).
best_exit_clock[] array IST hour histogram: actual exits vs optimal (the hour the peak actually landed in). A tall actual bar at 15:00 against a tall optimal bar at 10:00 is a client exiting on the clock rather than on the move.
biggest_leaks[] array Up to 12 rows, ranked by ₹ opportunity, grouped by Time of day / Day of week / Instrument / Duration / Tag. Groups with fewer than 3 trades are dropped — a single bad exit is luck, not a pattern.

Per-trade points

The scatter behind the aggregate — one row per covered trade. This is what you plot as MFE-vs-P&L and MAE-vs-P&L.

GET /users/{user_ref}/exit-analytics/trades

Takes the four filters above, plus:

Parameter Type Default Description
limit integer 500 Max points, newest first. Capped at 2000.
all_holds bool false Include multi-day holds. Off by default — see Why intraday.
{
  "user_ref": "CLIENT00423",
  "scope": "intraday",
  "count": 3,
  "total": 38,
  "truncated": true,
  "trades": [
    {
      "trade_id": "1725e1b8-623d-45dd-9376-1db4336a1f51",
      "instrument": "CHOLAFIN",
      "date": "09 Mar 2026",
      "exit_time": "2026-03-09T14:31:46+05:30",
      "segment": "EQ",
      "direction": "BUY",
      "pnl": 2221.6,
      "win": true,
      "mfe_inr": 2301.6,
      "mae_inr": 1608.4,
      "mfe_price": 23.016,
      "mae_price": 16.084,
      "mfe_atr": 0.427,
      "mae_atr": 0.299,
      "exit_efficiency": 0.9652,
      "t_mfe_min": 305,
      "t_mae_min": 88
    }
  ]
}

That row is a well-managed trade: ₹2,302 was available at the peak, ₹2,222 was booked, so exit_efficiency: 0.9652 — 96.5% of the move kept. It also went ₹1,608 against the position first (mae_inr), 88 minutes in, which is the part a P&L column never shows you.

Field Type Description
count / total integer Returned vs matching the filters.
truncated bool true when limit cut the list. Raise limit or narrow range.
exit_time string ISO-8601 in IST (+05:30). Rows are ordered newest-first on this field.
date string The same instant, pre-formatted for display.
pnl float Realised ₹, gross. The y-axis.
mfe_inr / mae_inr float How far the position went in your favour / against you during the hold, in ₹. Both are positive magnitudes, never signed P&L — mae_inr: 1608.4 means it was ₹1,608 underwater at its worst, and 0 means it never went against you at all. The x-axis.
mfe_price / mae_price float The same two magnitudes, in points per unit. Also both positive.
mfe_atr / mae_atr float \| null The same two, divided by ATR(14) at entry. Use these to compare across instruments — ₹ excursions are dominated by position size, so a NIFTY lot and 50 shares of SBIN are not comparable in ₹ but are in ATR. null when ATR was unavailable.
exit_efficiency float \| null pnl ÷ mfe_inr, clamped to 0–1. 0 means the entire favourable move was given back.
t_mfe_min / t_mae_min integer Minutes from entry to the best / worst point. t_mfe_min well below the hold length is the signature of holding too long.

The one chart worth building first

Plot mae_atr (x) against pnl (y). Trades clustered at high adverse excursion with a positive outcome are the ones the client survived rather than managed — the same size and the same setup, repeated on a worse day, is where accounts go. It is the most persuasive chart in this API precisely because it says nothing about the future.

Coverage

An excursion needs the instrument's candles. When we don't have them, the trade is recorded as uncovered rather than guessed at, and the example above reflects a real split: 38 covered out of 53.

Coverage is good for index options and liquid cash equities, and thinner for small-caps and illiquid strikes. Three consequences worth building for:

  • Show "38 of your 53 trades" in the UI. A client who counts their own trades and gets a different number stops trusting the rest of the integration.
  • empty: true is a normal first response for a new client, not an error.
  • Coverage improves retroactively. The same call next month may cover trades it skipped today, so don't cache the response on your side beyond a session.

Why intraday by default

Best-exit and capture rate are scoped to same-session holds. Over a multi-day hold, "the best price it touched at any minute" is a number nobody could have acted on, and billing it as foregone profit makes a patient trader look reckless. The hold_longer_curve is where the multi-day question gets answered honestly — each horizon reported separately, never as a maximum.

Pass all_holds=true on the per-trade endpoint if you want swing trades in the scatter; use mfe_atr / mae_atr there, not the ₹ columns.

Errors

Code Meaning
403 Account suspended, or the stats feature isn't enabled.
404 user_ref was never provisioned.
422 segment, trade_type or range is not one of the values above, or limit is outside 1–2000.