Skip to main content

Tradovate: Exclusive Features & Execution Internals

Tradovate support is in alpha

The Tradovate destination is in alpha. Bugs are still present, and participating in the alpha means helping us find them. Report anything unexpected to support so we can fix it.

The Destinations page covers how routing works and which commands and fields each destination supports. This page goes deeper on the Tradovate side in two ways:

  1. Copy-paste examples for every capability that only exists on the Tradovate destination.
  2. Execution internals: exactly which Tradovate API requests CrossTrade sends for each command, so you can reason about latency and Tradovate's rate limits when designing high-frequency strategies.

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 (trailing there is done through ATM strategies). Tradovate supports it natively. Required fields: stop_price (the initial trigger) and trail_offset (the trail distance, in price units; 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). 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.

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

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-06-19T20:00:00Z;
destination=tradovate;

Broker-side limit-order timeouts (cancel_after)

cancel_after (minutes, 1 to 180, limit orders only) is implemented as a Tradovate-native scheduled cancel: immediately after the entry is accepted, CrossTrade submits a cancel with a future activation time. The timeout lives on Tradovate's servers, so it fires even if your machine and CrossTrade are both unreachable at that moment. If the order has already filled by then, the scheduled 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;

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.

key=your-secret-key;
command=cancel;
account=DemoAccount;
order_id=my-entry-42;
activation_time=2026-06-12T15: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.

Note that prop firm logins (Apex, Topstep, and other Tradovate-based funded firms) always belong on the Demo environment, including funded/PA accounts, since firm accounts are simulation accounts that the firm mirrors. Link each firm identity with Link Demo; see Connecting Tradovate.

Execution internals: what each command sends to Tradovate

Unlike the NinjaTrader path, where CrossTrade hands one instruction to the add-on, the Tradovate destination executes composite commands itself as a sequence of REST calls to Tradovate's API. The sequences below are exactly what runs in production.

Two shared resolution steps appear in many sequences:

  • Account resolution costs nothing. Webhook account names are resolved from CrossTrade's server-side cache, not from Tradovate. The only exception is the first signal after linking (or a name we have never seen, such as a brand-new account at your firm), which triggers one GET /account/list sync per linked identity.
  • Symbol resolution depends on the form you send. A concrete symbol (ESM6 or ES 06-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.
CommandRequests sent, in order
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 flatplace1) GET /contract/find (contract id), 2) POST /order/liquidateposition (closes the position and cancels that contract's working orders broker-side), 3) POST /order/placeorder or /order/placeoso for the new entry. A "no position to flatten" result on step 2 is tolerated and the entry still goes in. Typically 3 requests.
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, a live position read (3 GETs, as above), then one POST /order/placeorder for twice the open size in the opposite direction. When flat, it falls back to a plain entry if the alert carried action/qty/order_type, or fails cleanly if not. 5 requests.
flattenGET /position/list per account in scope (or per linked identity when no account filter), then one POST /order/liquidateposition per matching open position.
flatteneverythingThe flatten scan and liquidations above, plus order cleanup: with an account filter, GET /order/list and one POST /order/cancelorder per working order; without one, a single POST /user/canceleverything per linked identity.
cancelPOST /order/cancelorder. 1 request (the order_id you assigned at place time resolves from CrossTrade's own map, not from Tradovate).
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.
changePOST /order/modifyorder. When the alert omits qty or order_type, one GET /orderVersion/deps first to read the working order's current values (Tradovate requires both fields on every modify). 1 to 2 requests.
cancelreplacePOST /order/cancelorder, then POST /order/placeorder. The replacement is only sent when the cancel succeeded, so a filled original never turns into doubled exposure. 2 requests.
cancelandbracketGET /contract/find, a live position read (3 GETs, size before), GET /order/list, one POST /order/cancelorder per working order on the instrument, a second live position read (the safety re-check), then POST /order/placeoco (or a single leg). Around 9 requests plus one per cancelled order.

Three multipliers on top of the per-command numbers:

  • Position and size gates. require_market_position adds a contract lookup plus a live position read (3 GETs); max_positions adds GET /position/list.
  • 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. Every successful leader action is mirrored to each configured follower, which costs roughly the same requests again per follower (contract lookup, entry or cancel, brackets).

CrossTrade-side controls never cost Tradovate requests: alerts blocked by trade windows, the kill switch, rate_limit, monitors, or validation errors are rejected before any Tradovate call is made. Authentication also adds nothing per order, since access tokens are kept warm by a background renewal job.

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". To close that gap, CrossTrade re-checks each placed 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. The Tradovate order id is recorded on the row in both cases, so support can trace any order at the broker.

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. 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 does not publish fixed request quotas. Instead its API applies dynamic penalties: when you send too much too fast, requests start returning HTTP 429 with a penalty ticket (p-ticket) and a cooldown duration (p-time).

What CrossTrade does with that:

  • A rate-limited request fails the alert immediately, and Alert History shows the exact reason: rate limited by Tradovate, retry in Ns.
  • Order-placing and order-cancelling requests are never auto-retried. A retry storm against a penalized session digs the hole deeper, and blindly resending an order that may have reached the broker risks a double fill. Read-only lookups are retried once on transient server errors.
  • Every error from Tradovate (rejections, penalties, outages) is recorded on the signal in Alert History with Tradovate's own reason text.

Practical guidance for staying clear of penalties:

  • Prefer single-purpose alerts over composites when firing frequently. A plain place is one request; a cancelandbracket on a busy instrument can be eight.
  • 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.
  • Multiply before you ship: 1 alert × 3 accounts × 2 copier followers is 9 executions' worth of requests arriving at Tradovate inside a second.
  • Use CrossTrade's own rate_limit + id fields to throttle a noisy TradingView strategy before it ever reaches Tradovate.
  • Send concrete contract symbols (ESM6) 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, so a linked identity stays usable indefinitely. If Tradovate revokes the session anyway (password change, firm-side reset, or you unlink at Tradovate), orders for that identity fail until you re-link, and CrossTrade sends you a critical Broker Auth Expired alert (in-app, plus email or Discord if configured) the moment the dead session is detected. Re-link from My Account, Brokers tab.