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