Changelog
Every change you can see, newest first. Additive changes never break a client that ignores unknown fields; breaking ones are announced before they ship.
- Additive
Pocketful, Tradejini and 5paisa types from both prod tenants
- Conformance: 70 entries from the Cirrus and Pocketful-tenant unhandled lists are promoted (mirrored locally): Pocketful trade rows (7 types), positions rows with the
actual_*carry fields null or numeric, WebSocket order frames (39; they trigger a book read), 7 modify and 1 NFO bracket request shapes, 400 / 429 / 502 reply envelopes; a Tradejini BFO order row withsym.weekly; a 5paisa account record. Mapped rows were checked against the raw rows; no existingexpect/file changed. - New Pocketful error templates: modify
margin shortfall(insufficient_margin), modifyorder status mismatched (FillQty)(transient), placetrigger price cannot be greater than last trade
price
(invalid_price_or_qty), book lookup 403 gateway page (transient),exceeded max request limit.429 on margins, trades and place (rate_limited), and placeyou're not allowed to place order in exchange 'BFO' / 'NFO'(outcome_unknown until a class is decided). - Conformance: 70 entries from the Cirrus and Pocketful-tenant unhandled lists are promoted (mirrored locally): Pocketful trade rows (7 types), positions rows with the
- Additive
Paper v2 stop-loss rests as a broker SL order
- On paper accounts, a v2 stop-loss (with or without a target) now rests as a broker SL order (
routestop_order); the server holds the target. Examples re-recorded:create_trigger_v2(success) andvalidate_trigger_v2(success) showroutestop_orderandroute_reason"broker stop-loss order holds the stop-loss, a server-side trigger the target" (wastrigger, "the broker's stop-loss orders are not proven ..."). Follows paper'sstop_orderfact being verified (8e17d4b6).
- On paper accounts, a v2 stop-loss (with or without a target) now rests as a broker SL order (
- Additive
Paper book rows of triggered and SL-M stops
- Conformance: a paper order book recorded on the local stack (account E2EP1, e2e
trigger_v1.stop_order_gapped_past_its_limit_is_exited_by_cirrus) is promoted. Its new handled types: an SL SELL MIS rowOPEN(a triggered stop resting at its limit after the price gapped past it; readOPEN, filled 0) and SL-M rowsCOMPLETE(BUY and SELL, readFILLED) andCANCELLED(SELL). Mapping unchanged.
- Conformance: a paper order book recorded on the local stack (account E2EP1, e2e
- Additive
Paper stop-loss orders proven (stop_order protection)
- Conformance: paper's
stop_orderprotection fact is verified (place_modify_cancel) from a live run ofp.stop_order_lifecycleon the local stack (account E2EP1): a plain SL under a held MIS lot placed, modified, read TRIGGER PENDING and cancelled. New paper recordings and expectations: the SL place, the SL modify request (a handled type now), its cancel and the book row of a cancelled SL. - On paper accounts (paper money only),
POST /v1/triggers(routeautoorstop_order) on an open intraday position now rests a broker SL (kindstop_order) instead of a Cirrus trigger, as on Pocketful;route: stop_orderis no longer refused there.
- Conformance: paper's
- Additive
Pocketful and Tradejini order book rows from production
- Conformance: order book rows and order-stream frames seen in production at the 2026-10-30 open are handled types, each with a promoted recording. Pocketful: a REJECTED MIS MARKET SELL NFO book row (reads REJECTED with the broker's margin message), and REJECTED / MODCONFIRMED MIS MARKET SELL order frames (a frame only triggers a book read). Tradejini: NFO normal SELL stop-limit rows that are cancelled (reads CANCELLED), rejected (REJECTED with the RMS reason) and open with the new
sym.weeklyfield (TRIGGER_PENDING, unfilled). No adapter behaviour changes.
- Conformance: order book rows and order-stream frames seen in production at the 2026-10-30 open are handled types, each with a promoted recording. Pocketful: a REJECTED MIS MARKET SELL NFO book row (reads REJECTED with the broker's margin message), and REJECTED / MODCONFIRMED MIS MARKET SELL order frames (a frame only triggers a book read). Tradejini: NFO normal SELL stop-limit rows that are cancelled (reads CANCELLED), rejected (REJECTED with the RMS reason) and open with the new
- Additive
Tradejini GTT book shapes from production
- Conformance: the Tradejini pending GTT book (
gtt_list) replies seen in production after #170 enabled the read are handled types, each with a promoted recording:{d:[rows],s}with Single GTT rows (single,stopLoss,targetblocks),{"d":[],"s":"ok"}, and{"s":"no-data","msg":"No Data Available."}. The last two read as an empty book (no GTT pending), not an error. No adapter behaviour changes.
- Conformance: the Tradejini pending GTT book (
- Additive
Error classes for broker read and GTT-cancel failures
- Conformance error classes (owner decision 2026-10-29) for replies recorded 2026-10-26 to 2026-10-29, which were
outcome_unknownuntil now: transient(a failed read; the next poll reads again): Dhan trades HTTP 502, 5Paisa positions and trades HTTP 504, AngelOne trades and positions HTTP 502 (gateway HTML pages), Nubra order book HTTP 500rpc error: code = Unavailable.transientfor Tradejini GTT cancel HTTP 200 "Please try again": a retryable refusal. The protection cancel reads the pending GTT book back: the record ends when the GTT is gone, otherwise it stayscancel_pendingand the cancel is sent again at the re-check.session_expiredfor paper positions and trades HTTP 401 "Invalid token", as for the paper order book.- AngelOne order book HTTP 404 "not found" stays
outcome_unknown. - Alice Blue: an unresolved holding's mapping issue names the field it was looked up by (
nseInstrumentId/bseInstrumentId) and its id, notinstrumentId <missing>. - Nothing users see changes, beyond retry and session handling of these replies.
- Conformance error classes (owner decision 2026-10-29) for replies recorded 2026-10-26 to 2026-10-29, which were
- Additive
Conformance corpus for 26-29 Oct prod replies; sub-lot quantities refused
- Conformance corpus: production broker replies seen 2026-10-26 to 2026-10-29 are recorded (angelone, aliceblue, pocketful, fivepaisa, dhan, groww, nubra, paper, tradejini). Only new recordings and expectations; no existing one changed. Current code reads each of them, so nothing users see changes.
- New catalogue scenario
p.gtt_delete_twice(every broker): deleting a GTT the broker no longer holds must leave it gone. - A refused GTT modify or delete is read back before its refusal is reported to protection (paper "GTT is not active", prod 2026-10-27).
POST/PATCH /v2/triggers, its validate call and/v1/triggersrefuse a bracket or ladder leg, a ladder rungqtyor a ladder remainder smaller than one lot with the existing 422invalid_request"quantity is smaller than one lot (lot size N)". Such a trigger could never execute (prod 2026-10-27: "Quantity rounds to 0 (lot size 1250)"). Response shape and codes unchanged.
- Additive
Broker capabilities gain `supports_gtt_read`
- Each broker's pinned capabilities (trigger hash
capabilities,/v1/accounts) gainsupports_gtt_read: whether Cirrus can read a broker GTT back (pending list or by id). True for angelone, dhan, nubra, paper, pocketful, tradejini and zerodha; false for the others. No field removed or renamed.
- Each broker's pinned capabilities (trigger hash
- Additive
5paisa margin refusals with whole-rupee amounts
- An order 5paisa's RMS refuses for want of margin with a whole-rupee amount (
... margin required is Rs230079, ...) is now classedinsufficient_marginlike one with paise (Rs7733109.67), instead of "not classed as definite" (prod 2026-10-29). Only the 5paisa conformance pack's error template changed.
- An order 5paisa's RMS refuses for want of margin with a whole-rupee amount (
- Additive
A refused GTT cancel is read back by the GTT itself on more brokers
- When Angel One, Dhan, Nubra, Pocketful or paper refuse to cancel a GTT that protects a position, the GTT is now read back by itself (Angel One
ruleDetails, Nubra byintentOrderId, the others from their GTT list), as on Kite, before falling back to the GTT list: a GTT that fired reads as fired, one that ended as ended. Before, a GTT the list no longer showed (Angel One lists only waiting / sent rules) stayed pending. A refused GTT change is read back the same way where the broker's pack classes the refusal asorder_not_modifiable(Kite, paper). Codes and shapes are unchanged.
- When Angel One, Dhan, Nubra, Pocketful or paper refuse to cancel a GTT that protects a position, the GTT is now read back by itself (Angel One
- Breaking
Every new protection is a v2 trigger
POST /v1/triggers(create_trigger), the order ticket'sprotection(POST /v1/orders, every user, every route but a broker bracket / cover), a protection made onPOST /v2/triggers, the protection of a signal webhook's entry (TradingView, Chartink, Kuberhunt; every route but a broker bracket / cover) and the protection put back after a broker refused a protection's exit are now always native v2 triggers; none writes a v1 trigger record any more. Protections made before keep their v1 records and work exactly as before (listed, read, changed and deleted on/v1/triggersand/v2/triggers, exited on the v1 path).POST /v1/triggersanswers as before (201,TriggerView), from the v2 trigger:trigger_idis the v2 trigger's id (a ULID; the same protection is on/v2/triggers/{trigger_id});kindsays where the stop-loss rests now (gtt_oco/gtt_single/stop_order, orplatform); a target a one-leg GTT or a stop order cannot hold is held by the same trigger (paired_trigger_idnull,target_priceset, one record instead of two). A trailing positional protection now keeps a broker GTT backstop too (was a server-side trigger only).504broker_outcome_unknownnow names the v2 trigger, which keeps watching the levels (nothing else is placed).fallbackrejectcancels the v2 trigger. No v1 exit worker is needed (503trigger_worker_downis no longer answered by this route).GET /v1/triggers/GET /v1/triggers/{trigger_id}also list / read those v2 protections in v1's shape (after the v1 records; one deleted through v1 is listed ended at once,ended_reasondeleted_by_user).PATCH /v1/triggers/{trigger_id}on such a protection changes the v2 trigger's levels: itsquantitycannot change and its stop-loss is never loosened (422invalid_request, fieldquantity/rules); a broker-held target cannot change (422, cancel and protect again).DELETE /v1/triggers/{trigger_id}/DELETE /v2/triggers/{trigger_id}on a v2 protection cancel its broker order first (strict sync: one the broker may still hold keeps the protection,502; fired,409), then the trigger (broker_cancelcancelledwhen a broker order was ended).POST /v2/triggers(create_trigger_v2): the broker part of a held bracket is placed by v2 itself (GTT, or a broker stop order for an intraday position where proven) and read back before the202(route,route_reason);trigger_idis always the v2 trigger's id (no v1 display record).GET /v2/triggers/{trigger_id}showsroutegtt/stop_orderfrom the protection's broker order.- The order ticket's
slices[].trigger_v2_idis now set for every protected slice the server protects on fill (routestriggerandgtt; v2 places the GTT for the filled position). - Every exit the server places for a position (v1 and v2 triggers,
POST /v1/positions/exit, signal closes) first ends a v2 protection's broker order on it (a GTT as well as a stop order). - Signal webhooks (Kuberhunt included): a protected entry's protection is found, removed by a close (broker order first, then the v2 bracket; later fills of that entry stay unprotected) and moved by a new stop / target through v2; the close, update and report answers keep their shapes (
protectionremoved/none_live/broker_cancel_failed,updateupdated/invalid/ ...). A new stop looser than the live one isinvalid(v2 never loosens a stop). Kuberhunt's outcome rows are unchanged (trigger_idis still the broker bracket's id, else empty). - Stored: v2 protection records may hold
order.kindstop_order(new value), new record kindtrg2_v1_face(oms:trg2:{ENV}:v1_face:{trigger_id}), order protection records an optionalclosed, new keysoms:trg2:{ENV}:successor:{mirror_id}. Deploy: production runs one OMS replica, so no older pod reads them during the rollout. Rolling back to an older build is the risk: it cannot read astop_orderprotection record (its exits wait, reported unreadable); cancel or let those protections end before a rollback. - Still on v1:
/v1/gttdisplay records, a broker bracket / cover placed with an order or a signal entry, records made before 2026-10-28 (they finish on v1, including a same-id hand-over of such a record to a server-side trigger), and Kuberhunt's own reco triggers written by api-kh. Notified: not required: Kuberhunt only consumes webhooks (no webhook, stream or signal payload changes); app and API users read the same shapes.
- Additive
GTT ids may contain `:`
PATCH /v1/gtt/{trigger_id}(modify_gtt) andDELETE /v1/gtt/{trigger_id}(delete_gtt) accept atrigger_idwith:, as Tradejini's GTT ids have (gtt:<n>,oco:<n>, returned byPOST /v1/gttandGET /v1/gtt). They were refused with 422invalid_requestbefore the broker was asked, so a Tradejini GTT could be neither moved nor deleted through the API. The refusal's message now names:among the allowed characters; ids with/,?,%or&are still refused.
- Additive
broker capability values pinned as a contract fixture
No wire value changed. The capability claims of every broker are now pinned in
- Additive: the pin lists kotakneo and nubra (added on main) with today's values.
- Pinned at the values each broker reports today, including paper's bracket/cover/GTT support from 2026-10-12.
- Additive
Per-strategy exit settings and an opt-in market fallback
- Strategy document format 1 (
docs/api/strategy.schema.v1.json, every route that takes or returns a document): optionalspec.execution.exitonprotected_limit@1, written out only when given:buffer_pct(0-5),chase.every_ms(200-10000),chase.max_slippage_pct(0-10),chase.max_attempts(1-50) (each absent: the entry setting),retry_every_ms(2000-60000, default 10000),market_after_s(nullor 30-900, default off) andescalate.telegram_s(30-3600, default 60),escalate.all_channels_s(60-7200, default 180),escalate.ops_s(120-14400, default 600). Documents without it normalize, compile and hash exactly as before. - Validation: out-of-bounds values are errors; escalation times that do not rise (
telegram_s<all_channels_s<ops_s, unset ones at their defaults) are aninvalid_combinationerror atspec.execution.exit.escalate. GET /v2/strategies/components(list_auto_strategy_components): theprotected_limitparameters listexitand its fields; its summary mentions the market fallback when set.- With
market_after_s, an exit leg still not flat that long after the exit started (market hours only) has its resting order cancelled and the remainder sent once as MARKET (market protection on), journaled asexit_market_fallbackon the deployment's journey, with a new notification "Exit sent at market" (eventexit_placed, severitywarning). The stored engine plan gains an optionalexecution.exit(readers first: written only when the document sets it). - How exits are placed, retried and escalated:
docs/api/strategy-execution.md(new).
- Strategy document format 1 (
- Breaking
Strategy validation and dry-run catch what deploys then skip
POST /v2/strategies/dry-run: strategies with indicator conditions now preview their legs (the preview compiled the unit without its watched symbols, so every one answered "indicatoremahere is not allowed").POST /v2/strategies/validatewithaccounts: an e2e test account on a real instrument (or a real account on an e2e instrument) is now a blocker (invalid_request, the deploy's words;deployable: false) instead of passing and being refused at deploy. Itsfieldis the account (accounts[i].account) or the e2e symbol's document field (spec.underlying.name,spec.watch.<name>,spec.legs.items[i].symbol). The same 422 fromPOST /v2/strategies/{strategy_id}/deploymentsnow names that field instead ofcirrus_token. The operation's description indocs/api/openapi.json(validate_auto_strategy) now says so: the other account rules stay warnings.- Adjustment conditions may read
cycle.minutes(the engine always allowed it; validation refused it as in an entry condition) and are checked as the engine runs them (every tick, no hold). - New
invalid_combinationerror onspec.reentry.until: an intraday strategy's re-entries must end by the latest square-off (15:20 NSE / BSE, the commodity's on MCX); a positional one's inside market hours.until: "23:59"on NFO was accepted before. - One message for too many watched symbols, counting
uwhether written or not: "Watched symbols may have at most 4, u included (found N)" (was "may have at most 4 (found 5)" or "Watch at most 3 symbols besides u (found 4) ...", depending on the case). The limit is unchanged. - New warnings (
needs_ack: false, nothing to acknowledge; the deploy is unchanged):sizes_to_zeroonspec.sizing.risk_rupeeswhenrisk_per_tradeis below what one lot loses at a rupee combined stop that bounds the loss (uncovered calls, options in more than one expiry, futures or shares), so every entry would size to 0 lots (the engine's own check, one formula);roll_expiry_neveronspec.adjustments[i]when aroll_expiryadjustment sits with a leg that names a fixed contract, which the roll cannot re-pick in another expiry. roll_expiryadjustments roll future legs: a future closes and reopens in the next listed future of the underlying (same side, lots from the position), alongside the options moving to the next expiry; shares stay held. A future leg no longer drawsroll_expiry_never.- An exact expiry (
expiry: {date}) of an option or future leg that today's instrument list does not have for the underlying is anot_in_instrument_listblocker onspec.legs.items[i].expiry, naming the nearest listed expiries; the deploy refuses it too (422not_deployable). Every entry used to skip withstock_expiry_unknown. - A future reference's default
uand@future(NAME, n)symbols skip contracts expired by now (today's after the 15:30 close), as leg selection does;uused to bind to yesterday's expired future. Notified: not required: Kuberhunt only sends webhooks
- Breaking
Webhooks, notifications, watchlists scopes grantable; activity summaries name instruments
Scopegainswebhooks,notifications,watchlists(read and enforced since "Webhooks, notifications, watchlists and signal modes open to API keys" below).POST /v1/api-keysnow accepts them. They are for the account holder's own API keys only: partner app registration (create_partner_app) refuses them (422invalid_scope, "scopewebhooksis for the account holder's own API keys; partner apps can't request it"), thepartner_oauthscheme does not list them, and the operations they guard no longer listpartner_oauthinsecurity. They are opt-in: no existing key gains anything. Breaking for strict clients becauseScopeis also sent back:GET /v1/api-keysandGET /v1/user/profileshow them when held, and an app session's profile (auth.scopes) now lists all six grantable scopes. The 400 for an unknown scope now lists all six.strategies,strategies_liveandstrategies_signalstay not grantable.PUT /v1/notifications/preferences,POST /v1/watchlists,PUT /v1/watchlists/{watchlist_id},DELETE /v1/watchlists/{watchlist_id}: only the account holder's own session or API key (as the webhook writes): a partner token, or an operator acting as the user, answers 403own_session_required. A partner never could hold these scopes, so for partners nothing changes; operator sessions lose these writes.PUT /v2/signal-modes/{endpoint_id}: switching back todirectwith an API key lackingstrategies_livestill answers 403live_not_enabled, now with the message "Direct mode places real orders: it needs the strategies_live scope on this credential".- Activity records (
GET /v1/activity,GET /v1/activity/{id}, the/v1/streamactivitytopic): new records'summarynames instruments as people read them (Buy 1 RELIANCE at ₹2,500 (Limit) in 1 account,NIFTY 27 Oct 22500 PE) instead of the trading symbol (RELIANCE-EQ,NIFTY27OCT202622500PE). Records written before keep their text;instrument.tradingsymbolandmessageare unchanged. Notified: NOT YET. Partners (Kuberhunt) must be told before this deploys.
- Breaking
Strategy validation matches what the engine deploys
- Strategy documents (
POST /v2/strategies,PATCH /v2/strategies/{strategy_id},/validate,/dry-run,/import,/versions) and the published JSON Schema (docs/api/strategy.schema.v1.json) now state the limits the engine enforces at deploy: spec.legs.items: at most 6 (was 8 in the schema; the server already refused more than 6).hold_sof condition sources: 0 to 300 (was 0 to 3600).spec.watch: at most 4 entries,uincluded, so at most 3 besidesu(was 5).spec.notify: at most 3, one per notifier type (was 4, which could never be reached: duplicates were already refused).- Timeframe
1dis no longer offered:GET /v2/strategies/capabilitiestimeframesand itstimeframeslabels drop1d, and a condition using'1d'is aninvalid_conditionerror (the engine never accepted it at deploy). - Capabilities
limits.max_watch_symbolsis 4 andlimits.max_notifiersis 3;GET /v2/strategies/componentsshows the newhold_sand leg-count maxima. - New
invalid_combinationerrors (the document is not saved): a lock step locking more than the profit that triggers it, lock steps that do not rise, a combined trail giving back more than it arms at, move to cost at or past the combined target, a leg's price stop on the wrong side of its price target for its side, an exact expiry date already past. The trail and lock-ladder cases wereengine_refusesblockers before (saved, not deployable). - A condition that does not compile is reported as
invalid_conditionwith its compile error even when it namescycle.minutes(it used to be saved with a misleading "isn't available" blocker). - Indicator conditions count only the watched symbols they read (with
u, at most 3): declaring more symbols than conditions read no longer blocks every indicator condition; a deployment watches only the symbols its conditions read. Notified: TODO (not yet told; fill in when and how before this ships)
- Strategy documents (
- Additive
Notifications say why, name instruments, and thread on Telegram
Notification(GET /v1/notifications, the/v1/streamnotificationstopic and thenotificationwebhook'sdata): new optional fields, absent on older notices and when unknown:source(NotificationSource:kindstrategy,trigger,protection,risk_limit,signal,manual,apiorpartner, other values may appear; optionalname,role(entry,exit,stop_loss,trailing_stop,target,alert,adjustment, other values may appear),rung,rungs),order(NotificationOrder:symboland optionalname,exchange,side,product,order_type,quantity,filled_qty,average_price),account_tag(the account's name in the app),realized_pnl(an exit's realised P&L in rupees, only when known from both fills) andthread(an opaque id shared by notices about one order chain, trigger or strategy run).- Notification text (
title,what_happened,what_we_did,what_you_should_do) never shows a Cirrus token: instruments are named as people read them (NIFTY 27 Oct 22500 PE,NIFTY 27 Oct FUT,RELIANCEforRELIANCE-EQ). Field values (order.symbol, tokens in other payloads) are unchanged. - Telegram (not an API): one template per event (what happened, why, the order, the account tag, the time, an "Open in app" button), no filler lines, and every message about one thing replies to the first one.
- Additive
v1 protection routes deprecated in favour of `/v2/triggers`
POST /v1/triggers,GET /v1/triggers,GET /v1/triggers/{trigger_id},PATCH /v1/triggers/{trigger_id},DELETE /v1/triggers/{trigger_id}(create_trigger,list_triggers,get_trigger,modify_trigger,delete_trigger):deprecated: true; each description now opens with its v2 replacement (/v2/triggers) and the field mapping. They keep working unchanged.- Every answer of these routes (errors included) carries
Deprecation: trueandLink: </docs/guides/protect-a-position>; rel="deprecation". NoSunsetdate is set yet; one will be announced here first. - Migration guide (v1 → v2 field mapping, with examples):
docs/api/MIGRATION-v1-protection.md, for the docs site's/docs/guides/protect-a-position. - Not deprecated:
/v1/gttandGET /v1/orders/{order_id}/protection.