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
Zerodha positions no longer carry a booked 0
- Positions of Zerodha accounts (
GET /v1/positions, thepositionswebhook and stream messages) leave outrealised_pnlinstead of sending0. Kite sendsrealised: 0on every row, closed ones too, with the whole P&L inunrealised(2,228 production rows, never another value), so that 0 was never a booked P&L.realised_pnlis optional and already absent for brokers that do not report it;pnlandunrealised_pnlare unchanged. Trigger MTM for Zerodha now derives booked P&L from the traded values instead of reading 0.
- Positions of Zerodha accounts (
- Additive
Strategy switches, MCX hours, stock safety checks and gallery eligibility
GET /v2/strategies/capabilities(get_auto_strategy_capabilities): newswitches(gallery,portfolio_risk,mcx,stock_fno,equity,stock_universe, eachtrue/falseonly, never the configuration behind them) andmcx_sessions(thelateandearlyMCX hours with their square-off cap and default, and per commodity inmcx_underlyingsitssessionand hours).- New
GET /v2/strategies/stock-status(get_auto_stock_status): for 1 to 50 NSE symbols, today's F&O ban, ex-date and circuit band checks as the engine enforces them, eachclear/blocked/unknown(fail closed) with the engine's waitingcode, andnew_entries. - New
GET /v2/strategies/{strategy_id}/gallery/eligibility(get_auto_strategy_gallery_eligibility): per badge (paper,live) the finished trades of a saved version and whether they reach the 20 a listing needs, with the reason when not.
- 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}) anddelete_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_watchlistreports each member'sstatus(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): optionalspec.stock.watchlist_id(24 characters) andspec.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.underlyingis the stock previews and backtests use. New validation blockers:watchlist_not_available(whileOMS_AUTO_EQUITYis off orOMS_AUTO_STOCK_UNIVERSEis 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; 422not_deployable(existing code) when the watchlist is gone or no member is tradable.AutoDeploymentgains optionaluniverse(watchlist_id,stocks,skippedwith each member'sreason);AutoUnitViewandAutoUnitLivegain optionalstock(andexchangeon 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.
- New operations (tag
- Additive
gallery examples re-recorded (docs only)
The
list_gallerysuccess example no longer shows a stray listing that another test run had left in the shared test database when it was first recorded (2026-10-22); the gallery examples' strategy name now carries a per-run id. No request, response field or behaviour changed. - Breaking
Broker-held protection copies the broker's state on delete (`cancel_pending`)
delete_trigger, trigger status / v2 statecancel_pending, ended statusfired_at_broker)DELETE /v1/triggers/{id}(delete_trigger) andDELETE /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_cancelcancelledwith the broker's refusal inbroker_message(wasfailed); - shown fired: ends as
fired_at_brokerand the delete answers409trigger_fired(the trigger shows fired, not cancelled); - otherwise (still live, or not readable): kept live, answered
202withdeleted: 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 (cancelledorfired_at_broker).broker_cancelfailednow 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 viewsstate):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_triggergainsbroker_cancel_pending(202) andbroker_fired(409);broker_cancel_failedis 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.
- Additive
Motilal Oswal calls send the login's access token
- Account records (
ACCOUNT_LIST_DATA): cirrus-oms now reads Motilal Oswal's top-levelAccess Token(written by broker-auth since 2026-10-08, Motilal'sgetaccesstokentoken) and sends it as theaccesstokenheader 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.
- Account records (
- 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.
- Conformance corpus: a Kotak Neo trade-book read that got no reply (connection dropped,
- Additive
Paid gallery templates on the credits wallet, behind a switch
only with the new
OMS_AUTO_GALLERY_PAID, off by default)- Listings (
list_gallery,get_gallery_listing,list_my_gallery_listings,publish_auto_strategy_to_gallery,withdraw_gallery_listing) gaineffective_price,is_free,price_reason(paid_disabled,promotion,admin_free,unpriced,priced) andnon_refundable;price_creditsis now the effective price (0 while paid templates are off, during a promotion, or when staff made it free: the stored price is kept and applies again). Your own listings also carryasking_price_creditsandadmin_free.price=free|paidonlist_galleryfollows the effective price. publish_auto_strategy_to_gallery:price_creditsabove 0 is refused (422,field: price_credits) while paid templates are off or outside the tenant's limits (OMS_AUTO_GALLERY_MIN_PRICE/_MAX_PRICE).purchase_gallery_listingnow charges the buyer's platform credits wallet (payment-handler) and makes the copy in the same step: new optional response fieldsreceiptandcopy; new answers409
purchase_failed
/purchase_in_progressand503 credits_unavailable(nothing charged). Purchases are non-refundable; a copy that fails is never charged. The author receivesOMS_AUTO_GALLERY_AUTHOR_SHARE_PCT(default 70) of each sale in credits.get_creditsanswers 403 while paid templates are off and503
credits_unavailable
when the wallet can't be read.- New operations:
get_gallery_pricing(GET /v2/gallery/pricing),list_gallery_purchases(GET /v2/gallery/purchases, receipts),get_gallery_earnings(GET /v2/gallery/earnings). - Not public:
GET /internal/v1/gallery/copies/{charge_id}for payment-handler (X-Internal-Service-Secret).
- Listings (
- Breaking
Broker stop orders protect existing intraday positions; v2 simple protection at the broker
kindvalue on triggers)POST /v1/triggers(create_trigger): withrules.routeauto(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:kindstop_order,exits: false, the stop order's id inbroker_order_id,quantityas 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 (kindplatform). New examplebroker_stop_order.rules.routeacceptsstop_orderonPOST /v1/triggers(refused with 422 whereautowould not take it; refused on orders).ProtectionRoute(an order'sprotectionroute) andRequestedRoutegainstop_order.PATCH /v1/triggers/{id}moves a stop order's stop-loss at the broker (still SL); its quantity follows the position.DELETEcancels 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/exitanswers503service_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(defaulton;offrestores the earlier behaviour exactly, per tenant without a deploy). POST /v2/triggers(create_trigger_v2): a simple protection (abracketon one leg already held,entry_pxwithoutentry, a plain price stop-loss and / or target, no trail, no other condition, noon: close/hold_s, novalid_until/strategy/watch) is now placed at the broker by the rules ofPOST /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; aCARRYFORWARDGTT never covers more than the broker holds), a broker stop order forMISwhere proven and switched on. It is placed before the202, whosetrigger_idis 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 fieldroute(RequestedRoute, defaultauto;triggerkeeps the earlier behaviour exactly). New status504broker_outcome_unknown(nothing else created). New examplebroker_gtt(create and get).TriggerV2Acceptedgains optionalroute(ProtectionRoute:gtt,stop_order,trigger),route_reason, and on a cancel of broker-held protectionbroker_cancel/broker_message.TriggerV2(GET /v2/triggers,GET /v2/triggers/{id}) gains optionalroute/route_reasonfor brackets and ladders (triggerfor 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, novalid_until; new statuses502broker_refused,504broker_outcome_unknown,409broker_held);DELETEcancels it at the broker (a stop order read back final) and removes it with its paired target (new status502; afterwardsGETanswers404, it is not kept listed ascancelled).- 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: triggeropts a request out. Notified: PENDING — owner to notify Kuberhunt/partners before deploy
- Additive
Tradejini realised P&L on the original cost; position `day_pnl`
- Position rows (
PositionRow,SharedPositionRow: positions reads, portfolio snapshots,/v1/streamsnapshots): new optionalday_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_pnlof a carried quantity closed today is now on its original cost, the basis of the row'spnl: Tradejini's ownrealizedPnlbooks 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_basisfor every broker (a closed row's realised equals its overall P&L within ₹1).
- Position rows (
- 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 fixtureadopted_mark.
- New Redis hash
- 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.
- 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.
- 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
- Additive
MCX strategies usable behind their switch
GET /v2/strategies/capabilities(get_auto_strategy_capabilities): everysegments[]entry gainslatest_square_offanddefault_square_off(HH:MMIST; MCX 23:00 / 22:45, NSE / BSE 15:20 / 15:15;nullforCDS, which strategies never trade). Nothing is removed: theCDSsegment and its label stay as before (available: false). WhileOMS_AUTO_STRATEGIES_MCXis on, a newmcx_underlyingsarray (same shape asunderlying_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 anot_in_instrument_listblocker. A document onCDSis refused as an unsupported segment (it always was refused; decision 11 drops currency). - Deployment live view
whysegment_not_availablenow reads "MCX strategies are switched off right now" (free-form text).
- 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,
normalplace 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'sNot ok/ "Failed" reads and HTTP 500 trade-book read, and 5paisa's Akamai HTTP 500 / 503 reads, aretransient. No REST, webhook or stream field changes; these no longer raise unhandled-type entries.
- 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,
- 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): optionalspec.stock, written out only when given:exit_days_before_expiry(0-10 trading days, default 2),cover(strategydefault, orany),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: newenums.stock_cover/labels.stock_cover(strategy,any).- Validation blockers (new codes, the document still saves):
stock_fno_not_available(stock options / futures whileOMS_AUTO_STOCK_FNOis off),equity_not_available(spec.stockon share legs whileOMS_AUTO_EQUITYis off),stock_not_allowed(a stock not inOMS_AUTO_STOCK_UNIVERSE),cnc_sell_unowned(a positional share sell),uncovered_call(a sold call without a share buy leg undercover: strategy),stock_settings_not_stock,capital_without_shares. Index and MCX documents are unchanged. - Deployment live view
why(free-form text) gains the waiting codesstock_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 reasonexpiry_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: falsewith the stock switch blocker untilOMS_AUTO_STOCK_FNO/OMS_AUTO_EQUITYare on and RELIANCE is in the stock universe.- A single stock's F&O document with
spec.stockmay also have a stock buy leg (a buy-write); it trades on the stock's cash exchange.
- Strategy document format 1 (
- Additive
Overall risk limits route; public strategy gallery and credits (both off)
- New operations
get_auto_risk_limits/set_auto_risk_limits(GET/PUT /v2/strategies/risk-limits, behindOMS_AUTO_PORTFOLIO_RISK, default off): the caller's max daily loss, max open exposure andclose_on_breach(default false) across every automated strategy;PUTkeeps fields left out and removes a limit sent asnull. It writes the existingoms:auto:{ENV}:portfolio_limitsrecord, which gains an optionalclose_on_breach(older engines ignore it and only block entries). - Open exposure per trade is now the most the structure can lose where bounded (spreads, bought options), else the broker's margin quote, else the entry value. The
entry_outcomeengine command gains an optionalmarginand the stored cycle state an optionalmargin(readers first). Withclose_on_breach, a breached daily loss closes every open trade: new exit reasonportfolio_losson stored / NATS engine records (readers first: older readers seeother; not in any REST enum). - New operations behind
OMS_AUTO_GALLERY(default off), app sessions only:publish_auto_strategy_to_gallery(POST
/v2/strategies/{id}/gallery
),list_gallery(GET /v2/gallery),list_my_gallery_listings(GET /v2/gallery/mine),get_gallery_listing/withdraw_gallery_listing(GET/DELETE /v2/gallery/{listing_id}),copy_gallery_listing(POST /v2/gallery/{listing_id}/copy),purchase_gallery_listing(POST /v2/gallery/{listing_id}/purchase,Idempotency-Keyrequired) andget_credits(GET /v2/credits). New error codes, used only by these operations:listing_not_found(404),not_enough_cycles,listing_pending,purchase_required,insufficient_credits(409).- New stored records (Mongo):
auto_gallery_listings,auto_gallery_purchases,auto_credit_accounts,auto_credit_entries. Staff routes/admin/v1/gallery*and/admin/v1/users/{u}/credits(not part of this document).
- New operations
- 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 whenOMS_AUTO_PORTFOLIO_RISKis 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
whymay show newportfolio_*waiting codes (free-form string).
- New stored record
- 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 carryingmarket_protection, a modify request and itssuccessreply, 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 classedorder_not_modifiable, like the cancel refusal. No REST, webhook or stream field changes; these no longer raise unhandled-type entries.
- 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
- Additive
The high-IV iron condor template deploys as shipped
GET /v2/strategies/templates(list_auto_strategy_templates): thehigh_iv_iron_condortemplate now uses only released components and functions (entryvix.ltp > 16 && adx(14, '15m') < 20watchingNSE_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.