Skip to content
Cirrus home
Sign in

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.

  1. 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 with sym.weekly; a 5paisa account record. Mapped rows were checked against the raw rows; no existing expect/ file changed.
    • New Pocketful error templates: modify margin shortfall (insufficient_margin), modify order status mismatched (FillQty) (transient), place trigger 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 place you're not allowed to place order in exchange 'BFO' / 'NFO' (outcome_unknown until a class is decided).

  2. 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 (route stop_order); the server holds the target. Examples re-recorded: create_trigger_v2 (success) and validate_trigger_v2 (success) show route stop_order and route_reason "broker stop-loss order holds the stop-loss, a server-side trigger the target" (was trigger, "the broker's stop-loss orders are not proven ..."). Follows paper's stop_order fact being verified (8e17d4b6).
  3. 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 row OPEN (a triggered stop resting at its limit after the price gapped past it; read OPEN, filled 0) and SL-M rows COMPLETE (BUY and SELL, read FILLED) and CANCELLED (SELL). Mapping unchanged.
  4. Additive

    Paper stop-loss orders proven (stop_order protection)

    • Conformance: paper's stop_order protection fact is verified (place_modify_cancel) from a live run of p.stop_order_lifecycle on 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 (route auto or stop_order) on an open intraday position now rests a broker SL (kind stop_order) instead of a Cirrus trigger, as on Pocketful; route: stop_order is no longer refused there.
  5. 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.weekly field (TRIGGER_PENDING, unfilled). No adapter behaviour changes.
  6. 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, target blocks), {"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.
  7. 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_unknown until 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 500 rpc error: code = Unavailable.
    • transient for 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 stays cancel_pending and the cancel is sent again at the re-check.
    • session_expired for 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, not instrumentId <missing>.
    • Nothing users see changes, beyond retry and session handling of these replies.
  8. 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/triggers refuse a bracket or ladder leg, a ladder rung qty or a ladder remainder smaller than one lot with the existing 422 invalid_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.
  9. Additive

    Broker capabilities gain `supports_gtt_read`

    • Each broker's pinned capabilities (trigger hash capabilities, /v1/accounts) gain supports_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.
  10. 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 classed insufficient_margin like one with paise (Rs7733109.67), instead of "not classed as definite" (prod 2026-10-29). Only the 5paisa conformance pack's error template changed.
  11. 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 by intentOrderId, 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 as order_not_modifiable (Kite, paper). Codes and shapes are unchanged.
  12. Breaking

    Every new protection is a v2 trigger

    • POST /v1/triggers (create_trigger), the order ticket's protection (POST /v1/orders, every user, every route but a broker bracket / cover), a protection made on POST /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/triggers and /v2/triggers, exited on the v1 path).
    • POST /v1/triggers answers as before (201, TriggerView), from the v2 trigger: trigger_id is the v2 trigger's id (a ULID; the same protection is on /v2/triggers/{trigger_id}); kind says where the stop-loss rests now (gtt_oco / gtt_single / stop_order, or platform); a target a one-leg GTT or a stop order cannot hold is held by the same trigger (paired_trigger_id null, target_price set, one record instead of two). A trailing positional protection now keeps a broker GTT backstop too (was a server-side trigger only). 504 broker_outcome_unknown now names the v2 trigger, which keeps watching the levels (nothing else is placed). fallback reject cancels the v2 trigger. No v1 exit worker is needed (503 trigger_worker_down is 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_reason deleted_by_user).
    • PATCH /v1/triggers/{trigger_id} on such a protection changes the v2 trigger's levels: its quantity cannot change and its stop-loss is never loosened (422 invalid_request, field quantity / 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_cancel cancelled when 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 the 202 (route, route_reason); trigger_id is always the v2 trigger's id (no v1 display record). GET /v2/triggers/{trigger_id} shows route gtt / stop_order from the protection's broker order.
    • The order ticket's slices[].trigger_v2_id is now set for every protected slice the server protects on fill (routes trigger and gtt; 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 (protection removed / none_live / broker_cancel_failed, update updated / invalid / ...). A new stop looser than the live one is invalid (v2 never loosens a stop). Kuberhunt's outcome rows are unchanged (trigger_id is still the broker bracket's id, else empty).
    • Stored: v2 protection records may hold order.kind stop_order (new value), new record kind trg2_v1_face (oms:trg2:{ENV}:v1_face:{trigger_id}), order protection records an optional closed, new keys oms: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 a stop_order protection record (its exits wait, reported unreadable); cancel or let those protections end before a rollback.
    • Still on v1: /v1/gtt display 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.
  13. Additive

    GTT ids may contain `:`

    • PATCH /v1/gtt/{trigger_id} (modify_gtt) and DELETE /v1/gtt/{trigger_id} (delete_gtt) accept a trigger_id with :, as Tradejini's GTT ids have (gtt:<n>, oco:<n>, returned by POST /v1/gtt and GET /v1/gtt). They were refused with 422 invalid_request before 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.
  14. 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.
  15. 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): optional spec.execution.exit on protected_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 (null or 30-900, default off) and escalate.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 an invalid_combination error at spec.execution.exit.escalate.
    • GET /v2/strategies/components (list_auto_strategy_components): the protected_limit parameters list exit and 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 as exit_market_fallback on the deployment's journey, with a new notification "Exit sent at market" (event exit_placed, severity warning). The stored engine plan gains an optional execution.exit (readers first: written only when the document sets it).
    • How exits are placed, retried and escalated: docs/api/strategy-execution.md (new).
  16. 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 "indicator ema here is not allowed").
    • POST /v2/strategies/validate with accounts: 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. Its field is 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 from POST /v2/strategies/{strategy_id}/deployments now names that field instead of cirrus_token. The operation's description in docs/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_combination error on spec.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 u whether 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_zero on spec.sizing.risk_rupees when risk_per_trade is 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_never on spec.adjustments[i] when a roll_expiry adjustment sits with a leg that names a fixed contract, which the roll cannot re-pick in another expiry.
    • roll_expiry adjustments 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 draws roll_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 a not_in_instrument_list blocker on spec.legs.items[i].expiry, naming the nearest listed expiries; the deploy refuses it too (422 not_deployable). Every entry used to skip with stock_expiry_unknown.
    • A future reference's default u and @future(NAME, n) symbols skip contracts expired by now (today's after the 15:30 close), as leg selection does; u used to bind to yesterday's expired future. Notified: not required: Kuberhunt only sends webhooks
  17. Breaking

    Webhooks, notifications, watchlists scopes grantable; activity summaries name instruments

    • Scope gains webhooks, notifications, watchlists (read and enforced since "Webhooks, notifications, watchlists and signal modes open to API keys" below). POST /v1/api-keys now accepts them. They are for the account holder's own API keys only: partner app registration (create_partner_app) refuses them (422 invalid_scope, "scope webhooks is for the account holder's own API keys; partner apps can't request it"), the partner_oauth scheme does not list them, and the operations they guard no longer list partner_oauth in security. They are opt-in: no existing key gains anything. Breaking for strict clients because Scope is also sent back: GET /v1/api-keys and GET /v1/user/profile show 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_live and strategies_signal stay 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 403 own_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 to direct with an API key lacking strategies_live still answers 403 live_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/stream activity topic): new records' summary names 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.tradingsymbol and message are unchanged. Notified: NOT YET. Partners (Kuberhunt) must be told before this deploys.
  18. 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_s of condition sources: 0 to 300 (was 0 to 3600).
    • spec.watch: at most 4 entries, u included, so at most 3 besides u (was 5).
    • spec.notify: at most 3, one per notifier type (was 4, which could never be reached: duplicates were already refused).
    • Timeframe 1d is no longer offered: GET /v2/strategies/capabilities timeframes and its timeframes labels drop 1d, and a condition using '1d' is an invalid_condition error (the engine never accepted it at deploy).
    • Capabilities limits.max_watch_symbols is 4 and limits.max_notifiers is 3; GET /v2/strategies/components shows the new hold_s and leg-count maxima.
    • New invalid_combination errors (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 were engine_refuses blockers before (saved, not deployable).
    • A condition that does not compile is reported as invalid_condition with its compile error even when it names cycle.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)
  19. Additive

    Notifications say why, name instruments, and thread on Telegram

    • Notification (GET /v1/notifications, the /v1/stream notifications topic and the notification webhook's data): new optional fields, absent on older notices and when unknown: source (NotificationSource: kind strategy, trigger, protection, risk_limit, signal, manual, api or partner, other values may appear; optional name, role (entry, exit, stop_loss, trailing_stop, target, alert, adjustment, other values may appear), rung, rungs), order (NotificationOrder: symbol and optional name, 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) and thread (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, RELIANCE for RELIANCE-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.
  20. 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: true and Link: </docs/guides/protect-a-position>; rel="deprecation". No Sunset date 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/gtt and GET /v1/orders/{order_id}/protection.