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
Quantities that are not whole lots are rounded down
POST /v1/orders, order modify,POST /v1/gtt,PATCH /v1/gtt/{trigger_id}andPOST /v1/orders/bracket: aquantityin units that is not a multiple of the lot size is rounded down to whole lots instead of refused withquantity must be a multiple of lot size N(lot 20, quantity 28 sends 20).POST /v1/ordersreports the units placed in each result'squantityand records them in the order's activity; modify, GTT and bracket answers carry no quantity. With an account multiplier, the units are scaled first and then rounded down per account (as before). Less than one lot is still refused with 422invalid_request, now wordedquantity is smaller than one lot (lot size N)(fieldorders[i].quantityon place,quantityelsewhere).POST /v1/positions/exitis unchanged: a position that is not whole lots is still refused with its remainder. - Additive
strategy-level risk limits never flatten
Where strategy-level risk limits are enabled (
OMS_TRIGGERS_V2_STRATEGY_RISK, off),POST /v2/triggers,PATCH /v2/triggers/{trigger_id}andPOST /v2/triggers/validateaccept them only withon_breachpauseornotify;flatten(and noon_breach, which meansflatten) is a 422spec.on_breach. Nothing changes while the switch is off, and account / user limits are unchanged. - Additive
Nubra adapter; its conformance pack
Nubra accounts (
BrokerNubra;ACCOUNT_LIST_DATAAccess Tokenand the newDevice ID, written by broker-auth's Nubra login; a static IP is needed) are served like other brokers: same request and response shapes,GET /v1/accountsreports themsupported, updatespolling. Orders go to Nubra's REST V3 intent orders (NUBRA_BASE_URL, defaulthttps://api.nubra.io); MTF, GTT, bracket / cover and position conversion are refused as for any broker without them. No existing field, type, enum value or status code changes. The broker conformance corpus gains a newnubrapack atreplay_green: synthetic recordings of the order book, trades, positions, holdings, funds, place / modify / cancel, margin and the 400 refusal from Nubra's API docs, their expectations, the documented order statuses frozen, and that 400 ("Enter valid inputs to proceed.") classedinvalid_field. Live replies (2026-10-13/14) are read as Nubra sends them: positionsnetQty/buyQty/sellQty, holdingsqty(the docs namenetQuantity/quantity; both are read), a delivery buy's blocked cashtotalMarginBlockedNew; holdings carry the ISIN. Nubra'sProtectionProfile(and the pack's[protection]) is every factunverified, like Groww. One 2026-10-13 margin quote recording is revoked (a float that hashed differently by build features); a live margin quote replaces it. - Additive
strategy-level risk limits
POST /v2/triggers,PATCH /v2/triggers/{trigger_id}andPOST /v2/triggers/validateaccept arisk_limitwithlevel: strategy(and astrategy) when the server enables it (switchOMS_TRIGGERS_V2_STRATEGY_RISK, off); elsewhere the answer stays 422spec.levelwith today's message. While the strategy's numbers cannot be trusted the limit does not fire and new entries of the strategy are refused (the trigger's entry showsfailedwith the reason); nothing is flattened. Nothing changes while the switch is off. - Additive
strategy on order rows
Order rows (
OrderData:GET /v1/orders,GET /v1/orders/{order_tag}, every response and/v1/streammessage that embeds an order) may carry an optionalstrategy: the strategy of the v2 trigger that placed the order. Absent otherwise. Order webhooks are unchanged. - Additive
v2 entry updates and journey kinds (follow-up)
Recorded for commit 171eaad, which shipped them without a
Contract-Change:trailer: withOMS_TRIGGERS_V2_ENTERon, the owner'strigger_update(stream and opted-in webhooks) carries one update per entry outcome in the existing shape (from=to, noactions,reasonin words,journeyset,event_id{trigger_id}:entry:{event_id}:{leg}:{kind}). The journey (sourceentry) also gainsentry_resting,entry_resting_late,entry_rebase_skippedandentry_held_by_server. Nothing changes while the switch is off. - Additive
v2 trigger entries
GET /v2/triggers/{trigger_id}andGET /v2/triggersshow an optionalentryobject (TriggerV2Entry) on triggers whose entry the server placed (switchOMS_TRIGGERS_V2_ENTER, off):status(sending,accepted,filled,partially_filled,failed,outcome_unknown; other values may appear),filled_qty,avg_price,order_ids,reason. Absent otherwise; nothing changes while the switch is off. With it on, the owner'strigger_updatepush (stream and opted-in webhooks) also carries one update per entry outcome, same shape,from=to,event_id{trigger_id}:entry:{event_id}:{leg}:{kind}, and the journey gainssourceentry(kindsentry_sent,entry_adopted,entry_filled,entry_filled_late,entry_failed,entry_unknown,entry_not_protected). New exampleenteredonget_trigger_v2. - Additive
recorded error examples for v2 triggers
Documentation only: recorded examples for documented error responses of the
/v2/triggersoperations that had none.not_enabled(403) onget_trigger_v2,cancel_trigger_v2,modify_trigger_v2,pause_trigger_v2,resume_trigger_v2,validate_trigger_v2,get_trigger_capabilities_v2,get_trigger_journey_v2;engine_unavailable(503) on cancel, modify, pause and resume;ended(409) on resume;client_id(400malformed_json) on modify and validate;invalid_query(400) on the journey. Existing examples and response shapes unchanged. - Additive
partly filled exits in the v2 trigger journey
The journey (
GET /v2/triggers/{trigger_id}/journey, unreleased) records a newkindexit_partly_filled(sourceexit) when the exit worker reads part of a v2 exit filled:order_id(the order tags),filled_quantity,quantity(the exit's total) andaverage_price. One entry per distinct filled quantity of a placement. Thekinddescription inopenapi.jsonlists it. The legacy trigger history is unchanged. - Additive
Dhan / Zerodha margin requests, Upstox account record recorded
No request or response changes. New Dhan (BSE F&O, NSE equity) and Zerodha margin-quote request shapes and an Upstox account record seen in production join the conformance corpus, so they no longer alert.
- Additive
Pocketful tenant shapes recorded
No request or response changes. New Pocketful order book, positions, trades, place request and stream frame shapes seen on the Pocketful tenant join the conformance corpus, so they no longer alert.
- Additive
Paper, Alice Blue, Dhan, 5Paisa, Zerodha shapes recorded
No request or response changes. New broker reply shapes seen in production (Paper's first full trading session, new Alice Blue / Dhan / 5Paisa / Zerodha order rows) join the conformance corpus, so they no longer alert. Alice Blue "No holdings found" is read as an empty holdings list, and a Paper "Instrument data unavailable" refusal as a passing failure (nothing was placed).
- Additive
Pocketful MTF orders and positions are listed
Pocketful orders and positions with product MTF (placed in Pocketful's own app) are now listed with
product: "MTF", an existing value; they were left out of the order book and positions. Placing MTF through Pocketful is still refused. A Pocketful cancel or modify refused by the gateway's HTML 403 page is a passing gateway block, as reads and placements already are. - Additive
dead Motilal / 5Paisa logins pause; Dhan margin opening
A Motilal Oswal login the broker no longer accepts (HTTP 200
MO2032"Invalid AccessToken") and a 5Paisa login it refuses (head status2"Invalid head parameters.") now pause the account and ask for a new login, as an expired session already does at every other broker, instead of being polled every few seconds. Dhan's marginopening_balanceis now used + available (both include collateral), as for Alice Blue and 5Paisa; it was the cash-onlysodLimit, which madeutilisedread far above 100. Refusals newly classed from production replies (Zerodha, 5Paisa, Upstox, Pocketful, Alice Blue, Tradejini) keep the reasons users already see. No field, enum value or status code changes. - Additive
v2 trigger expression reference
Documentation only:
docs/api/trigger-expressions.md(variables incl.prev.*andlegs[i].*, functions incl.crosses_above/crosses_below, operators, limits, error messages, examples per kind) and the expression summary invalidate_trigger_v2's description. No shape changes. - Additive
v2 triggers show refused commands and ended protection
GET /v2/triggers,GET /v2/triggers/{trigger_id}(andtrigger_update'sprotection):- new optional
last_result(TriggerV2LastResult:op,command_id,outcomeapplied|duplicate|rejected,reason,at): the engine's answer to the last command it handled, so a refused modify, pause, resume or cancel shows on the trigger with its reason. Absent until the engine answered one (recorded examples unchanged). - an ended trigger's protection parts the server held show
statusended(an existing value) instead ofheld_by_server; broker parts keep what the broker shows.
- new optional
- Additive
exits and broker protection in the v2 trigger journey
The journey (
GET /v2/triggers/{trigger_id}/journey, unreleased) also records exit orders (sourceexit:exit_placedwith the order tags and quantity,exit_filled,exit_refused,exit_unfilled,exit_repriced,exit_retried,exit_failedwhen dead-lettered) and the broker protection (sourceprotection:gtt_placed,gtt_modified,gtt_cancelled,gtt_triggered,readback_matched,readback_mismatch,readback_unknown,degraded). Reasons are cut to 200 characters; no broker reply bodies. No other shape changes. - Additive
v2 engine notes in the trigger journey
The trigger runtime publishes notes about a trigger (an input not known, a stale tick ignored, the evaluation budget run out, a stored trigger quarantined) as new record kind
trg_noteon core NATS{ENV}.trg.note.{bucket}.{user}.{trigger_id}(no stream), at most the first per trigger, kind and minute and 1000 per worker per minute. The api journals them (sourceengine,kindnote) through one queue group. Fixtures and a reader test in - Additive
v2 trigger journey
New operation
GET /v2/triggers/{trigger_id}/journey(get_trigger_journey_v2,readscope; users/v2/triggersis enabled for): the caller's trigger's journey, newest first (TriggerV2Journey:entries[],next_cursor),?limit=(1-200, default 50),?cursor=,?source=(api,engine,result,exit,protection);400invalid_query,404trigger_not_found(also for another user's trigger),503. Each entry (TriggerV2JourneyEntry):entry_id,at,seq,source,kind,summary,actor(api entries: user, credential, API key id, partner client id, client IP),details(a modify's spec diff, a transition's explanation and values, a refusal's reason, ...). Kept 90 days by default (OMS_TRIGGERS_V2_JOURNEY_DAYS), also after the trigger ended.TriggerV2Update(stream / webhooktrigger_update, opt-in) gainsjourney: the entry the update adds. New stored record kindtrg2_journey(Mongo, append-only; fixtures and a - Additive
v2 engine events say why (explain)
Stored record
trg_event({ENV}.trg.evt.…) gains an optionalexplain{condition, source, values}on transitions a condition or a level decided: the spec path (stop_loss.when,target.when,entry.when,when,targets[n].price,stop_loss,mtm_floor,profit_lock.floor), the condition as written (at most 256 bytes) and up to 16 values read at that input (ltp,prev.ltp,sl_px, ...). Absent on older events and on other transitions; readers ignore it. Shadow events (trg_shadow_event) are unchanged. No REST, webhook or stream shape changes.