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

    Zerodha positions no longer carry a booked 0

    • Positions of Zerodha accounts (GET /v1/positions, the positions webhook and stream messages) leave out realised_pnl instead of sending 0. Kite sends realised: 0 on every row, closed ones too, with the whole P&L in unrealised (2,228 production rows, never another value), so that 0 was never a booked P&L. realised_pnl is optional and already absent for brokers that do not report it; pnl and unrealised_pnl are unchanged. Trigger MTM for Zerodha now derives booked P&L from the traded values instead of reading 0.
  2. Additive

    Stock watchlists; a stock strategy can run on one (switches off)

    • New operations (tag watchlists, app sessions only): list_watchlists (GET /v1/watchlists), create_watchlist (POST /v1/watchlists), get_watchlist (GET /v1/watchlists/{watchlist_id}), update_watchlist (PUT /v1/watchlists/{watchlist_id}) and delete_watchlist (DELETE /v1/watchlists/{watchlist_id}). They are the app's market watch lists (same store): up to 50 lists per user, up to 50 symbols each; symbols added here must be NSE / BSE cash equities in today's instrument list. get_watchlist reports each member's status (tradable, not_allowed, not_equity, not_listed). New error codes, used only by these operations: watchlist_not_found (404), watchlist_name_taken (409); instruments_unavailable (503, existing) while the instrument list has not loaded.
    • Strategy document format 1 (docs/api/strategy.schema.v1.json): optional spec.stock.watchlist_id (24 characters) and spec.stock.max_open_positions (1-50), written out only when given (documents without them normalize and hash exactly as before). A watchlist strategy trades shares of each member; spec.underlying is the stock previews and backtests use. New validation blockers: watchlist_not_available (while OMS_AUTO_EQUITY is off or OMS_AUTO_STOCK_UNIVERSE is empty), watchlist_shares_only, watchlist_id_invalid, max_open_without_watchlist.
    • Deploying a watchlist strategy (deploy_auto_strategy, go_live_auto_deployment) runs one unit per (account, tradable member), counted as one deployment; 422 not_deployable (existing code) when the watchlist is gone or no member is tradable. AutoDeployment gains optional universe (watchlist_id, stocks, skipped with each member's reason); AutoUnitView and AutoUnitLive gain optional stock (and exchange on the unit). All absent on other deployments.
    • New waiting codes in a unit's why (free-form string, not an enum), checked before every new trade of a watchlist unit (open trades keep every exit): watchlist_unknown, watchlist_deleted, watchlist_removed, watchlist_not_listed, watchlist_not_equity, watchlist_not_allowed, watchlist_unresolved, watchlist_full.
  3. Breaking

    Broker-held protection copies the broker's state on delete (`cancel_pending`)

    delete_trigger, trigger status / v2 state cancel_pending, ended status fired_at_broker)

    • DELETE /v1/triggers/{id} (delete_trigger) and DELETE /v2/triggers/{id} (cancel_trigger_v2) on broker-held protection (GTT, bracket / cover legs): a broker refusal no longer removes the trigger on its own. The protection is read back from the broker (the GTT, the broker's GTT list, the legs in the order book):
    • shown cancelled / ended at the broker: removed, broker_cancel cancelled with the broker's refusal in broker_message (was failed);
    • shown fired: ends as fired_at_broker and the delete answers 409 trigger_fired (the trigger shows fired, not cancelled);
    • otherwise (still live, or not readable): kept live, answered 202 with deleted: false, broker_cancel: "pending" and the broker's message, and re-checked every 30 s until the broker shows a final state, which the trigger then copies (cancelled or fired_at_broker). broker_cancel failed now only means the account is no longer linked. No answer / expired session is unchanged (502, kept).
    • Status values apps can show (GET /v1/triggers, GET /v1/triggers/{id} status; v2 views state): cancel_pending ("Cancel pending at broker": a delete waits on the broker, the protection can still fire), fired_at_broker (ended: the broker's GTT / legs fired), cancelled (ended: cancelled at the broker).
    • The same rule applies inside the server: a paired target's exit waits while the broker still holds its stop-loss GTT, and a signal close holds back a slice whose broker protection fired or is cancel-pending.
    • Examples: delete_trigger gains broker_cancel_pending (202) and broker_fired (409); broker_cancel_failed is removed (that answer is now only for an unlinked account). Notified: owner decision 2026-10-26 (relayed in this work); app-cirrus to show "Cancel pending at broker" before this deploys.
  4. Additive

    Motilal Oswal calls send the login's access token

    • Account records (ACCOUNT_LIST_DATA): cirrus-oms now reads Motilal Oswal's top-level Access Token (written by broker-auth since 2026-10-08, Motilal's getaccesstoken token) and sends it as the accesstoken header on every Motilal call, empty when absent. Since 2026-10-08 Motilal refused every call without it ("Invalid AccessToken In Header Parameter", MO2032) even right after a successful login, so the account was shown as logged out and stopped reading and trading every day. Nothing in REST responses, webhooks or stream messages changes.
  5. Additive

    Kotak Neo trade-book transport failure recorded

    • Conformance corpus: a Kotak Neo trade-book read that got no reply (connection dropped, failure: other) seen in production is recorded, like the order-book and positions ones already were. Current code already handles it (the read fails and is retried), so nothing users see changes.
  6. Breaking

    Broker stop orders protect existing intraday positions; v2 simple protection at the broker

    kind value on triggers)

    • POST /v1/triggers (create_trigger): with rules.route auto (default), an open intraday (MIS) position with a stop-loss and no trailing is now protected by a broker stop-loss order (SL, never SL-M) resting at the broker where the account's broker has proven stop-loss orders (Pocketful today), instead of a server-side trigger. It is answered as its display trigger: kind stop_order, exits: false, the stop order's id in broker_order_id, quantity as the broker holds it (never more than asked). A target is a paired server-side trigger (paired_trigger_id), as for a one-leg GTT. A stop order the broker refuses is replaced by a server-side trigger at once (kind platform). New example broker_stop_order.
    • rules.route accepts stop_order on POST /v1/triggers (refused with 422 where auto would not take it; refused on orders). ProtectionRoute (an order's protection route) and RequestedRoute gain stop_order.
    • PATCH /v1/triggers/{id} moves a stop order's stop-loss at the broker (still SL); its quantity follows the position. DELETE cancels it; a stop order that already went off is exiting at the broker and is kept (502, retry).
    • Every exit the server places for a position held by a stop order (POST /v1/positions/exit, trigger exits, signal closes) cancels the stop first and reads it back final; a stop that went off is never exited next to: the exit waits for it (about 3 s); once it filled only what the broker still holds is exited, else nothing is placed (POST /v1/positions/exit answers 503 service_unavailable, an existing code; trigger exits retry; signal closes hold the square-off back and the redelivery closes what is left).
    • Switch: OMS_PROTECT_STOP_ORDER (default on; off restores the earlier behaviour exactly, per tenant without a deploy).
    • POST /v2/triggers (create_trigger_v2): a simple protection (a bracket on one leg already held, entry_px without entry, a plain price stop-loss and / or target, no trail, no other condition, no on: close / hold_s, no valid_until / strategy / watch) is now placed at the broker by the rules of POST /v1/triggers (prod 2026-10-26: the app's "Add protection" on a Tradejini NRML option was a server-side trigger and no broker GTT was placed): a broker GTT for a positional product where the broker has GTTs (a one-leg-GTT broker holds the stop-loss, a paired server-side trigger the target; a CARRYFORWARD GTT never covers more than the broker holds), a broker stop order for MIS where proven and switched on. It is placed before the 202, whose trigger_id is then the protection's display record (a v1 trigger id, not a ULID). A broker refusal makes a server-side trigger at once. New request field route (RequestedRoute, default auto; trigger keeps the earlier behaviour exactly). New status 504 broker_outcome_unknown (nothing else created). New example broker_gtt (create and get).
    • TriggerV2Accepted gains optional route (ProtectionRoute: gtt, stop_order, trigger), route_reason, and on a cancel of broker-held protection broker_cancel / broker_message. TriggerV2 (GET /v2/triggers, GET /v2/triggers/{id}) gains optional route / route_reason for brackets and ladders (trigger for a v2 trigger the server evaluates). Broker-held v2 protection is listed with them.
    • PATCH /v2/triggers/{id} on broker-held protection moves it at the broker first (same parts, plain prices, no valid_until; new statuses 502 broker_refused, 504 broker_outcome_unknown, 409 broker_held); DELETE cancels it at the broker (a stop order read back final) and removes it with its paired target (new status 502; afterwards GET answers 404, it is not kept listed as cancelled).
    • Rollout: readers first: deploy with OMS_PROTECT_STOP_ORDER=off, then switch it on. v2 GTT routing has no switch of its own: it is live on deploy for v2 users; route: trigger opts a request out. Notified: PENDING — owner to notify Kuberhunt/partners before deploy
  7. Additive

    Tradejini realised P&L on the original cost; position `day_pnl`

    • Position rows (PositionRow, SharedPositionRow: positions reads, portfolio snapshots, /v1/stream snapshots): new optional day_pnl, today's P&L in rupees, a quantity carried in from earlier sessions counting from the previous close. Set for Tradejini only; absent elsewhere.
    • Tradejini realised_pnl of a carried quantity closed today is now on its original cost, the basis of the row's pnl: Tradejini's own realizedPnl books it against the previous close, so overall minus realised (what the app shows as unrealised) was wrong (prod MUR004, 2026-10-26; recording positions-c7e2653f: -3263 now -5817.5, 5850 now 9149.4). Rows with no carried quantity closed today are unchanged. Approved by the owner.
    • Conformance: new data invariant position.realised_on_overall_basis for every broker (a closed row's realised equals its overall P&L within ₹1).
  8. Additive

    Adopted-order marks kept with the order snapshot (internal)

    • New Redis hash oms:adopted:{user|account} (field = order tag, value {"at": <RFC 3339>}, every field optional), written by the engine when it adopts a broker row as an order placed outside Cirrus, pruned with the order snapshot. A restored book still knows the order is an outside one, so another outside order reusing its broker tag is adopted on its own instead of re-pointing it (prod 2026-10-15, 5paisa). Old pods ignore the key; without a mark an order is treated as before. Reader fixture adopted_mark.
  9. Additive

    Broker reply classes for read refusals (Tradejini, Dhan, Pocketful)

    • Conformance corpus: production broker replies seen 2026-10-23 to 2026-10-26 are recorded (dhan, fivepaisa, kotakneo, paper, pocketful, tradejini). Current code already reads each of them correctly, so nothing users see changes.
    • How some read failures are classed: Tradejini's HTML 401 "Authorization Required" page is a refused login, like its JSON 401 (the account is paused until the next login, as it already was). Dhan's 502 gateway page on a positions read is transient. Pocketful's 429 "Exceeded max request limit." on holdings and positions reads is rate limited. Before this change these were unclassified (outcome_unknown). They are internal classes and are never sent to users.
  10. Additive

    5paisa order and trade times read as UTC

    • 5paisa order-book and trade-book times (an order's placed time and broker update time, a trade's fill time) are now read as the UTC instants 5paisa sends. They came out 5 h 30 min early before (a fill at 09:21:25 IST read as 2026-10-21T22:21:25Z instead of 2026-10-22T03:51:25Z), and so did everything built from them (order and trade reads, webhooks, stream messages carrying them). Orders 5paisa's RMS refuses before the exchange times them (empty ExchOrderTime) keep their placed time, which 5paisa writes as IST wall-clock and was already read right. Other brokers are unchanged.
    • Conformance: new data invariant trade.filled_in_session (a fill before 09:00 IST) flags a broker time read in the wrong zone on any broker.
  11. Additive

    MCX strategies usable behind their switch

    • GET /v2/strategies/capabilities (get_auto_strategy_capabilities): every segments[] entry gains latest_square_off and default_square_off (HH:MM IST; MCX 23:00 / 22:45, NSE / BSE 15:20 / 15:15; null for CDS, which strategies never trade). Nothing is removed: the CDS segment and its label stay as before (available: false). While OMS_AUTO_STRATEGIES_MCX is on, a new mcx_underlyings array (same shape as underlying_details) lists every MCX commodity with a live future, priced off its nearest future.
    • Validation / deploy with MCX on: any listed MCX commodity (not only OMS_AUTO_UNDERLYINGS); agricultural contracts keep a 17:00 window and a 16:30 latest square-off; a missing MCX square-off is normalized to 22:45 (16:15 for agri); a commodity with no live future today is a not_in_instrument_list blocker. A document on CDS is refused as an unsupported segment (it always was refused; decision 11 drops currency).
    • Deployment live view why segment_not_available now reads "MCX strategies are switched off right now" (free-form text).
  12. Additive

    Conformance corpus from production (2026-10-21/22 sessions, 12 brokers)

    • Conformance packs gain prod_ledger recordings for shapes already read correctly: Kotak Neo (rejected F&O order-book row, order-feed frames, feed end, account record, place and margin-quote requests), Tradejini (order-book / trade rows, normal place and single-leg GTT requests, account records, a transport failure), 5paisa (intraday and BSE rows, "Waiting in Q on Exch Link", "Cancelled"), Alice Blue, Dhan, paper (MIS / MTF rows, holdings with T1 quantities), Motilal Oswal, Angel One, Groww and Upstox.
    • Refusal classes: Dhan DH-906 "Market is Closed! Want to place an offline order?" and Zerodha "Your order could not be converted to a After Market Order (AMO)" are market_closed; Alice Blue's Not ok / "Failed" reads and HTTP 500 trade-book read, and 5paisa's Akamai HTTP 500 / 503 reads, are transient. No REST, webhook or stream field changes; these no longer raise unhandled-type entries.
  13. Additive

    Single-stock strategy settings (stock options and futures, cash equity; switches off)

    • Strategy document format 1 (docs/api/strategy.schema.v1.json, every route that takes or returns a document): optional spec.stock, written out only when given: exit_days_before_expiry (0-10 trading days, default 2), cover (strategy default, or any), circuit_gap_pct (0.5-10, default 2), capital_per_trade (₹, optional), max_shares (1-100000, optional). Documents without it normalize and hash exactly as before.
    • GET /v2/strategies/capabilities: new enums.stock_cover / labels.stock_cover (strategy, any).
    • Validation blockers (new codes, the document still saves): stock_fno_not_available (stock options / futures while OMS_AUTO_STOCK_FNO is off), equity_not_available (spec.stock on share legs while OMS_AUTO_EQUITY is off), stock_not_allowed (a stock not in OMS_AUTO_STOCK_UNIVERSE), cnc_sell_unowned (a positional share sell), uncovered_call (a sold call without a share buy leg under cover: strategy), stock_settings_not_stock, capital_without_shares. Index and MCX documents are unchanged.
    • Deployment live view why (free-form text) gains the waiting codes stock_fno_not_available, equity_not_available, cnc_sell_unowned, corporate_actions_unknown, ex_date, fno_ban_unknown, fno_ban, stock_expiry_unknown, stock_expiry_window. A stock F&O trade inside its expiry window exits with the existing reason expiry_day_exit.
    • GET /v2/strategies/templates (list_auto_strategy_templates): five new templates, stock_ema_crossover, stock_opening_breakout, stock_mean_reversion, covered_call, cash_secured_put (existing categories and risk badges), deployable: false with the stock switch blocker until OMS_AUTO_STOCK_FNO / OMS_AUTO_EQUITY are on and RELIANCE is in the stock universe.
    • A single stock's F&O document with spec.stock may also have a stock buy leg (a buy-write); it trades on the stock's cash exchange.
  14. Additive

    Portfolio limits record; 25 more engine-evaluated functions; MCX strategies (off)

    • New stored record oms:auto:{ENV}:portfolio_limits (per-user max daily loss / max open exposure across deployments), read by the trigger runtime when OMS_AUTO_PORTFOLIO_RISK is on (default off). No writer route yet; no REST change.
    • Strategy and trigger capabilities (with indicator conditions on) mark 25 more functions available (session levels, pivots, opening range, rvol, hv, yz_vol, vortex, fisher, tsi, regime, ltp(sym), minutes('HH:MM')): values change, field names and types do not.
    • MCX intraday futures strategies behind OMS_AUTO_STRATEGIES_MCX (default off); capabilities show MCX available only when it is on.
    • Deployment live view why may show new portfolio_* waiting codes (free-form string).
  15. Additive

    Conformance corpus from production (Alice Blue, Angel One, paper, Tradejini)

    • Conformance packs gain prod_ledger recordings for shapes already read correctly: Alice Blue order-book rows (BFO / LONGTERM / REGULAR pairings), an Angel One account record still in account_setup, paper place requests carrying market_protection, a modify request and its success reply, NRML LIMIT order-book rows (OPEN, CANCELLED), and a Tradejini order-socket handshake ending on HTTP 502. Paper's HTTP 400 "Order is COMPLETE and cannot be modified" is classed order_not_modifiable, like the cancel refusal. No REST, webhook or stream field changes; these no longer raise unhandled-type entries.
  16. Additive

    The high-IV iron condor template deploys as shipped

    • GET /v2/strategies/templates (list_auto_strategy_templates): the high_iv_iron_condor template now uses only released components and functions (entry vix.ltp > 16 && adx(14, '15m') < 20 watching NSE_INDEX:INDIA VIX; rolls on a doubled premium; daily loss limit Rs 8,000), so it is deployable as listed. Field names and types are unchanged; the recorded example's description, components, indicators and summary changed.