Skip to main content

Tradovate: Exclusive Features & Execution Internals

The Destinations page covers routing and which commands and fields each destination supports. This page has copy-paste examples for every Tradovate-only capability, then the request sequence behind each command, so you can reason about latency and Tradovate's rate limits.

For multi-target scale-out brackets, see Tradovate ATM. Tradovate natively owns the multibracket, scale-out targets, fixed stops, OCO links, and continuous trails. CrossTrade adds named cloud templates, profit-triggered trail activation with restart-safe trigger memory, and target-fill breakeven. On Tradovate, strategy= / atm_strategy= names a CrossTrade template, not an NT8 or Tradovate-saved template.

CrossTrade prices Tradovate orders itself

CrossTrade never assumes the linked Tradovate login supplies market data. Relative-price conversion, position display marks, and managed ATM trigger activation are calculated by CrossTrade server-side from live market data, even when that login has entitlements. If fresh pricing isn't available, the operation fails closed or leaves the existing fixed stop unchanged. For triggered trails, trigger progress from before a restart is preserved, so activation evaluation resumes where it left off.

Everything here assumes a linked Tradovate identity (My Account, Brokers tab) and destination=tradovate; in the alert.

Tradovate-only features, with examples​

Native trailing stops​

NinjaTrader webhooks have no trailing order type. Tradovate supports it natively. Required fields: stop_price (the initial trigger) and trail_offset (the trail distance, in price points; peg_difference is an accepted alias).

key=your-secret-key;
command=place;
account=DemoAccount;
instrument=ES1!;
action=sell;
qty=1;
order_type=trailingstop;
stop_price=5285.00;
trail_offset=4.00;
tif=gtc;
destination=tradovate;

order_type=trailingstoplimit works the same way and additionally requires limit_price (the limit leg of the stop-limit), for example limit_price=5283.00; on the order above. All three price fields are required: stop_price, limit_price, and trail_offset. Keep the limit on the fill side of the stop: at or below the stop for a sell, at or above for a buy.

Market-if-touched (MIT)​

A touch-price order: it becomes a market order when the price trades at your level, without resting in the book like a limit. The touch price goes in limit_price (not stop_price).

key=your-secret-key;
command=place;
account=DemoAccount;
instrument=ES1!;
action=buy;
qty=1;
order_type=mit;
limit_price=5290.00;
tif=day;
destination=tradovate;
QTS is not supported

order_type=qts appears in Tradovate's API schema, but Tradovate has confirmed that placing a QTS order is not supported; every attempt fails at the broker. CrossTrade rejects the alert up front with a clear error instead of relaying Tradovate's bare HTTP 400.

Iceberg orders (max_show)​

Show the market less than your true size. max_show is the displayed quantity and must be greater than zero.

key=your-secret-key;
command=place;
account=LiveAccount1;
instrument=ES1!;
action=buy;
qty=10;
max_show=2;
order_type=limit;
limit_price=5288.00;
tif=gtc;
destination=tradovate;

Good-till-date orders (tif=gtd)​

In addition to the shared day and gtc, Tradovate accepts ioc (immediate or cancel), fok (fill or kill), and gtd. GTD requires expire_time as an ISO-8601 datetime, and CrossTrade rejects the alert up front if it is missing.

key=your-secret-key;
command=place;
account=DemoAccount;
instrument=NQ1!;
action=buy;
qty=1;
order_type=limit;
limit_price=18500.00;
tif=gtd;
expire_time=2026-12-18T20:00:00Z;
destination=tradovate;
GTD expiry on demo and prop-firm accounts

Live accounts expire GTD orders natively at expire_time. Tradovate's demo environment, which also hosts most prop-firm evaluation accounts, records expire_time but never enforces it. CrossTrade covers the gap: its servers watch the deadline and cancel the order themselves shortly after expiry passes, typically within half a minute.

Broker-side limit-order timeouts (cancel_after)​

cancel_after (minutes, 1 to 180, limit orders only) runs in two layers. Immediately after the entry is accepted, CrossTrade submits a Tradovate-native scheduled cancel with a future activation time, so on live accounts the timeout sits on Tradovate's servers and fires even if your machine and CrossTrade are both unreachable at that moment. CrossTrade's servers also track the same deadline and cancel the order themselves if it is still working shortly after the timeout passes; on demo and prop-firm accounts, where Tradovate accepts scheduled cancels but never executes them, this is the layer that actually fires. If the order has already filled by then, either cancel is a harmless no-op.

key=your-secret-key;
command=place;
account=DemoAccount;
instrument=ES1!;
action=buy;
qty=1;
order_type=limit;
limit_price=5286.00;
tif=day;
cancel_after=15;
destination=tradovate;
Partial fills on bracketed entries

Tradovate creates the take-profit and stop-loss legs of an OSO in a restrained state (Suspended), activates them on the entry's first fill, and sizes them to that fill. That applies to market entries that print in more than one fill as much as to limit entries that fill over time. CrossTrade watches every bracketed entry that requested a stop-loss, from the placement response onward, off Tradovate's live order and fill events plus a periodic server-side re-check:

  • If a protective leg is gone (completed, cancelled by hand, expired, or rejected) while the entry is still working and something has filled, CrossTrade cancels the remaining entry quantity and confirms the cancel with the broker.
  • If a protective leg is gone while the entry is still working and nothing has filled, the entry is left alone and you get one Position Protection alert.
  • Whenever the entry finishes, and after any such cancel, CrossTrade reconciles coverage (entry fills minus exit fills minus working stop quantity, clamped to the live position). Uncovered quantity gets a new OCO pair at your original take-profit and stop-loss prices and a Position Protection alert saying so. If the original take-profit is still working but the stop is gone, CrossTrade alerts instead of rebuilding, because a second target would double your exit.

This is recovery after the fact. Between a fill and the corresponding stop reaching Working there is always a broker-side interval, and quantity filled after the first fill is unprotected until the rebuild lands. A shorter CANCEL_AFTER narrows the window on limit entries but is not a substitute for this guard. Entries with a take-profit only are not watched, because nothing was asked to be covered.

Scheduled cancels (activation_time on cancel)​

You can also schedule a cancel of any working order for a future time yourself. activation_time is ISO-8601 and the cancel executes broker-side at that moment on live accounts. On demo and prop-firm accounts, Tradovate parks the schedule without ever running it, so CrossTrade enforces the same deadline server-side shortly after it passes.

key=your-secret-key;
command=cancel;
account=DemoAccount;
order_id=my-entry-42;
activation_time=2026-12-11T15:55:00Z;
destination=tradovate;
activation_time is a cancel field

activation_time schedules a cancel. It does not delay or schedule order entry. For delayed entries, use the shared delay= field, which works on both destinations.

Order labels (notes and cl_ord_id)​

notes is the free-form label field: it rides to Tradovate as the order's text and shows on the order inside Tradovate's own UI and reports, which makes bot orders easy to separate from manual ones at the broker. cl_ord_id (client order id) passes through as clOrdId. When you set an order_id for later cancels and changes, it is also sent as the cl_ord_id by default, so the order is traceable in Tradovate's interface.

custom_tag is rejected

Tradovate's API has a customTag50 order field, but it identifies registered B2B partners to the exchange; it is not a user fill tag. Normal API orders that carry it are rejected by Tradovate with an unhelpful UnknownReason, so CrossTrade rejects custom_tag up front with an error that explains why. Use cl_ord_id or notes instead.

Multiple identities, one webhook surface​

A single CrossTrade account can link several Tradovate identities at once (a personal login plus any number of funded-firm logins, Live and Demo both). You never tell the webhook which identity to use: the account= name is looked up across everything you have linked, and the order is routed to the identity and environment that owns that account. Names must be unique across linked identities. If two linked identities expose the same account name, CrossTrade rejects the order and asks you to rename one rather than routing unpredictably.

Prop firm logins, including funded/PA accounts, belong on Demo; see Linking Your Tradovate Account.

Execution internals: typical Tradovate request sequences​

Unlike the NinjaTrader path, where CrossTrade hands one instruction to the Add-On, the Tradovate destination executes composite commands as REST workflows. The table shows the typical broker sequence, not an exact request transcript. Durable recovery, settlement polling, cache misses, copier mappings, and account fan-out can add, skip, or resume reads and mutations depending on current state.

Two shared resolution steps appear in many sequences:

  • Account resolution normally adds no broker request. Webhook account names are resolved from CrossTrade's server-side cache. The first signal after linking, or a name the cache has never seen such as a brand-new account at your firm, can trigger one GET /account/list sync per linked identity.
  • Symbol resolution depends on the form you send. A concrete symbol (ESZ6 or ES 12-26) is passed through directly. The TradingView continuous form (ES1!) costs one GET /contract/suggest to resolve the front month. Commands that operate on an existing position or order book additionally need the contract id, which costs one GET /contract/find when it was not already learned from the suggest call.
CommandTypical broker workflow
placePOST /order/placeorder. With take_profit/stop_loss: a single POST /order/placeoso instead (entry plus brackets in one call). With cancel_after: one extra POST /order/cancelorder to schedule the timeout.
place with flatten_first=true, and flatplaceResolve the exact contract, liquidate the same-account position, then poll live net position and nonterminal same-contract orders until both are clear before placing the entry. A "no position" result is tolerated, but the order-clear barrier still runs. Each mapped follower performs its own flatten and settlement check before its entry.
closeposition (full)GET /contract/find, then POST /order/liquidateposition. 2 requests.
closeposition (partial, quantity/percent)GET /contract/find, a live position read (GET /position/find + GET /order/list + GET /fill/list, see Position freshness), then an opposing POST /order/placeorder market order for the computed quantity. 5 requests.
reverse / reversepositionGET /contract/find plus a live position read, POST /order/liquidateposition, bounded fill-derived polling until flat, then POST /order/placeorder for the new position. reverse derives the opposite side and original size; reverseposition uses the explicit entry fields. Both reject a flat starting position.
flattenA reconciled open-position scan per account (position, order, and fill reads), then one POST /order/liquidateposition per matching open position.
flatteneverythingThe flatten scan and liquidations above, plus order cleanup: GET /order/list per identity and one POST /order/cancelorder per working order. An account filter limits both phases to that account. CrossTrade deliberately does not call Tradovate's /user/canceleverything, which revokes API permissions rather than acting as a safe trading-only bulk cancel.
cancelPOST /order/cancelorder. Usually 1 request when the order_id you assigned at place time resolves from CrossTrade's own map. For a raw numeric broker order id without account, CrossTrade first runs GET /order/list on every linked identity and mutates only if exactly one account owns it. Supplying account avoids that fan-out and rejects a mapped id that belongs to another account.
cancelordersGET /order/list for the account's identity, then one POST /order/cancelorder per working order that matches (plus GET /contract/find when narrowed to an instrument).
cancelallordersGET /order/list per linked identity, then one POST /order/cancelorder per working order. Tradovate has no bulk cancel-all endpoint, so the sweep costs one request per working order.
changeRead /orderVersion/deps whenever needed to restore omitted quantity, order type, required limit/stop prices, or a required trailing offset, then POST /order/modifyorder. A raw numeric order id without account first requires GET /order/list on every linked identity to prove unique ownership. Mapped followers perform the same restoration from their own live versions, with follower sizing re-applied. A fully specified, account-scoped change can skip the leader version read.
cancelreplaceClaim the owner-fenced durable workflow, reconcile any prior recovery stage, verify that the original order belongs to the requested account and exact replacement contract, cancel and confirm the leader, then cancel and confirm every mapped signal-copier follower before replacement fan-out. Broker calls vary by recovery stage. An ambiguous cancel or placement is recorded and never resent; the response requires reconciliation.
cancelandbracketResolve the contract, read live position, list and cancel matching orders, poll until cancellations are terminal, re-read position for the safety check, then place the OCO (or single leg). Expect roughly 10 requests plus one per canceled order, with additional polling while settlement is pending.

Three multipliers on top of the per-command numbers:

  • Position and size gates. require_market_position adds a contract lookup plus reconciled position/order/fill reads; max_positions also uses the reconciled open-position scan rather than one position request.
  • Multi-account lists. account=A,B,C replicates the alert into one full, independent execution per account, so three accounts cost three times the requests.
  • The Tradovate Copier. A plain signal entry normally adds one placement per follower, so five followers add about 5 broker requests to the leader workflow. Symbol replacement, execution-mode fill handling, and position-aware copies can add contract, position, order, fill, and settlement reads. In order mode, every leader placement, change, and cancel fans out to the followers, so an actively worked order book costs proportionally more than one that is set and left. There is no cap on active followers per leader; the per-identity governor paces the fan-out instead. Follower results are independent: a failed follower never rolls back the leader, so don't resend the leader signal after a timeout (see Request load and partial failures).

CrossTrade-side controls normally cost no Tradovate requests: alerts blocked by trade windows, the kill switch, rate_limit, or validation errors are rejected before a trading call. Account Manager evaluation is the exception because it refreshes broker cash snapshots. An actively monitored account is polled about 6 times per minute; an unmonitored linked account gets an approximately once-per-minute display backstop. Authentication normally adds nothing because tokens are kept warm, but a request entering the renewal margin may renew inline before continuing.

Accepted, then rejected: the post-placement check​

Tradovate's placement endpoints can answer "accepted" (HTTP 2xx with an order id) before the risk and execution layer has finished judging the order. A price outside the product's exchange price limits is the common case: the order is accepted, then rejected about a second later with InvalidPrice.

A successful webhook response therefore means "Tradovate accepted the order", not "the order is resting or filled". For ordinary placements that return a top-level order id, CrossTrade re-checks that order a few seconds after acceptance with one background GET /order/item (it never delays your webhook response). If the order was rejected broker-side, the Alert History row gains a warning carrying Tradovate's reject reason and moves to the yellow warning status, and CrossTrade sends an Order Rejected alert through your alert channels. REST and MCP placements have no Alert History row, so an integration on those surfaces should read the order status or lifecycle itself a few seconds after placing if it needs the outcome synchronously. Native multibracket ATM strategies have their own strategy and child-order lifecycle instead of this single-order check.

Position freshness after a fill​

Tradovate's position/find can keep reporting the pre-fill state for tens of seconds after a fill, while the fill stream updates immediately. (The same lag reaches the REST surface: GET /v1/api/tv/positions and the singular GET .../position serve raw rows, while GET .../accounts/{account}/positions is fill-reconciled. See Freshness.) Commands that act on the live position (require_market_position, partial closeposition, a bare reverse, cancelandbracket, and copier follower sizing) therefore compute the live net position from the fill stream: three lightweight reads (position/find, order/list, fill/list) instead of one. Chained alerts that follow an entry within seconds act on the true position. A flat reading is re-checked once (about a second) before being trusted, so commands on a genuinely flat instrument cost one extra second.

Rate limits on Tradovate, and how CrossTrade behaves​

Tradovate runs two separate rate-limit systems, and they fail in different ways. Knowing which one you hit tells you how long to wait and whether retrying can ever help.

The user-level limit: HTTP 429 Too Many Requests​

Tradovate's partner documentation publishes a user-level limit of 5,000 requests per hour for an authenticated user, counted across every endpoint. Cross it and Tradovate answers HTTP 429 Too Many Requests with an empty body: no p-time, no header, nothing to read. Tradovate's guidance is to stop calling for a full hour, because every request sent during that hour, even a test, restarts the clock. A user-level 429 cannot be resolved programmatically.

The "80 requests per minute" figure that circulates in forums and prop firm help centers does not appear in Tradovate's published limits. Plan around the hourly numbers.

Endpoint limits: the penalty ticket (p-ticket)​

Each endpoint also has its own hourly limit, and going over one does not produce an error. Tradovate returns HTTP 200 OK with a penalty ticket in the body instead of the result you asked for, so a client that only checks the status code will think the request worked. A penalty ticket on order placement looks like this:

{
"p-ticket": "NTgsRzhnNFJS...",
"p-time": 2,
"p-message": "Rate limit exceeded: more than 1500 requests per hour"
}
FieldWhat it means
p-ticketThe penalty ticket: an encrypted token tied to the caller's IP address. Once the wait is over, the request is resent with this value added as an extra field.
p-timeSeconds to wait before retrying. It is additive: each further penalty ticket inside the window adds another back-off to the time remaining, so retrying early only lengthens the wait.
p-captchaWhen true, Tradovate wants a reCAPTCHA, which no server-to-server integration can answer. The only fix is to wait about an hour for the endpoint's count to clear.
p-messageTradovate's description of the limit you crossed, such as Rate limit exceeded: more than 1500 requests per hour. It can be general or missing; the failed call's violations and errorText fields carry the detail.

Per Tradovate's documentation, endpoint counts run on a rolling 60-minute window, so calls age out about an hour after they are made rather than resetting at the top of the hour. Penalty tickets are keyed to the caller's IP address (its /24 subnet), not to a user or API key. On most endpoints only failed calls count: bad credentials, malformed requests, non-existent accounts, 4xx and 5xx responses, and 200 responses that carry a business error. A smaller set of sensitive endpoints counts every call. Retrying through penalty tickets instead of waiting them out is the usual route to the user-level 429 above.

The 1,500 requests per hour limit on order placement​

Tradovate's published endpoint table does not list order placement, but the API states the limit itself. When POST /order/placeorder is over its count, the penalty ticket's p-message reads Rate limit exceeded: more than 1500 requests per hour. Traders running large clusters of evaluation accounts under one Tradovate login have reported the same ceiling on the Tradovate community forum.

1,500 placements an hour sounds like plenty until you multiply. A copier leader that places 100 orders an hour with 15 followers on the same login sends 1,500 placements, before counting bracket orders, re-entries, or the copier's own corrections. Rejected orders count toward the limit as well, so a follower that keeps getting refused (insufficient margin, a prop firm lock) uses up the hour fastest.

Every action counts toward a login's hourly budget, including order placements, modifications, cancellations, and session reconnects. Copy trading multiplies each action by the number of connected accounts, and active trailing stops send a steady stream of modification requests, so both drain the hour quickly.

CrossTrade therefore paces requests through an upstream governor for each linked (environment, Tradovate user ID) identity. The governor spreads bursts out (so copy fan-outs to many accounts under one login clear quickly), keeps headroom reserved for emergency liquidation and reconciliation, and prioritizes urgent actions over background reads. The exact pacing is internal, adapts to broker behavior, and may change without notice.

For Tradovate's own explanation, see Penalty Tickets and Rate Limits in the partner documentation, plus the older How Tradovate limits requests and data and How to handle request limits FAQ pages. Tradovate sets these limits and can change them.

What CrossTrade does when Tradovate penalizes a login​

  • Broker-penalized requests fail with a customer-safe Tradovate rate-limit message. The exact p-ticket, p-time, and p-message stay in internal diagnostics. While the penalty is active, the governor sheds background reads, reduces other admission rates, and any request that reaches the penalty gate fails fast instead of extending the penalty. After p-time expires, one request makes the broker's ticketed retry with the p-ticket attached.
  • In the Tradovate Copier Copy Log, a copy marked as skipped because the order could not be sent never reached Tradovate. It is not a broker rejection, and nothing was placed on that follower.
  • Ambiguous order mutations are not blindly resent. A retry storm against a penalized session digs the hole deeper, and resending an order that may have reached the broker risks a double fill. Read-only lookups may retry once on transient server errors. Idempotent copier entries can make one bounded retry while their client-order id and copy deadline are still valid.
  • The same discipline is yours on a plain place: neither Tradovate nor CrossTrade deduplicates it (Tradovate accepts a repeated cl_ord_id, and CrossTrade's idempotency keys cover only its own composite commands). Set order_id or cl_ord_id on every entry and reconcile through the order list and lifecycle before resending. The recipe is in Reconciling an ambiguous placement.
  • Every error from Tradovate (rejections, penalties, outages) is recorded on the signal in Alert History with Tradovate's own reason text.

Staying clear of Tradovate penalty tickets​

  • Do the placement math per login before going live: leader orders per hour times followers on that login, plus brackets, is what /order/placeorder sees. Keep it well under 1,500 an hour.
  • Fix what keeps getting rejected. Failed calls count toward endpoint limits, so a follower that repeatedly hits a margin or prop firm rejection eats the budget without ever placing a trade.
  • Prefer single-purpose alerts over composites when firing frequently. A plain place is typically one broker mutation; a cancelandbracket on a busy instrument can require ten or more calls plus settlement polling.
  • Scope your sweeps. Both cancelallorders and cancelorders cost one request per working order (Tradovate has no bulk cancel endpoint), so prefer cancelorders narrowed to an account and instrument over a global sweep.
  • Keep the operational load in mind: every active Account Manager monitor adds recurring cash-snapshot reads, and every active copier follower adds its own broker requests to each leader event, all drawing from the same per-identity budget.
  • Use CrossTrade's own rate_limit + id fields to throttle a noisy TradingView strategy before it ever reaches Tradovate.
  • Send concrete contract symbols (ESZ6) instead of ES1! in high-frequency alerts to skip the front-month lookup.
Fail-closed gates

If CrossTrade cannot read your live position while enforcing require_market_position or executing a reverse (for example during a Tradovate outage), the command fails with the error logged to Alert History. It never assumes "flat" and never places a fallback order on a failed read.

Authorization expiry​

Tradovate sessions are kept alive automatically by a server-side renewal job, including while you are idle. If Tradovate revokes or evicts a session (password change, firm-side reset, or unlink), requests fail until you re-link. After repeated confirmation failures, CrossTrade emits the matching broker-session or broker-auth alert through your configured channels. Re-link from My Account, Brokers tab.