tastytrade
A Rust client for the tastytrade brokerage API: accounts, balances, positions, transactions, instruments, option chains, market data, orders — and two real-time websockets.
Orders placed through this crate move real money. Certification is the default and production is a deliberate opt-in; see Certification is the default.
Install
[]
= "0.5"
Minimum supported Rust version: 1.88. It is declared as rust-version in
Cargo.toml and a CI job builds the library against exactly that toolchain, so
the two cannot drift.
Authentication
tastytrade decommissioned POST /sessions on 2026-02-11. Username and
password authentication, session tokens and remember tokens are gone from the
venue, and gone from this crate with them. If you have arrived from an older
version or an older tutorial looking for login(), this is why it is not here —
and there is no deprecated shim, because a deprecated login() would still call
an endpoint that no longer exists.
OAuth2 is the only flow, in two documented grants.
The personal refresh-token grant
Create an OAuth application and a personal grant under Manage → My Profile → API on my.tastytrade.com. That gives you a client secret and a refresh token.
# .env
TASTYTRADE_CLIENT_SECRET=...
TASTYTRADE_REFRESH_TOKEN=...
TASTYTRADE_USE_DEMO=true
use TastyTrade;
use TastyTradeConfig;
async
Access tokens last about fifteen minutes. You do not have to manage that: every
request renews the token in hand before it expires. A renewal is never a
retry — a POST that may have placed an order is not replayed on a 401.
The third-party authorization-code grant
For an application acting on somebody else's account. Send the customer to the authorization page, then exchange the code:
use TastyTrade;
use ;
use TastyTradeConfig;
# async #
Certification is the default
TastyTradeConfig::from_env selects certification
(api.cert.tastyworks.com). Production takes a literal opt-in:
TASTYTRADE_USE_DEMO=false # production — orders placed here are real
Only a value that parses as false selects production. A missing, empty or
misspelled variable resolves to certification, so a typo cannot be what points an
order at a funded account. Connecting without credentials fails locally and never
reaches the network.
A session is bound to the deployment it authenticated against: it will not present a certification token to production, and it will not send the client secret to a host it did not authenticate with.
What it covers
Every REST endpoint published in tastytrade's OpenAPI documents — 97 of 97.
Doc/API_Coverage_Status.md is the endpoint-by-endpoint matrix.
Accounts
Balances (one row per currency, plus a per-currency lookup), balance snapshots, filtered positions, trading status, transactions, net-liq history, margin requirements and risk parameters, and the customer resource.
# use *;
# async
The customer resource carries names, addresses, tax identifiers and birth dates.
Nothing in it renders itself — Debug and Display print a field count, and
reading a value means naming the field.
Instruments and option chains
Equities, equity options, futures, future options, their products, cryptocurrencies, warrants, flat/compact/nested chains, and symbol and instrument search. The listings paginate and take typed filters.
# use *;
# async
Market data
REST snapshots for up to 100 symbols without opening a websocket, market metrics (IV index, rank and percentile, liquidity), dividend and earnings history, and market sessions and holidays.
# use *;
# async
Market Metrics and Net Liq History are live only per the venue's sandbox page; their examples require an explicit read-only production opt-in.
Orders
The reviewed placement flow is the documented path:
# use *;
# async #
Why the receipt exists: it binds the account number and the base_url.
Certification reuses production account numbering, so without the origin a
sandbox dry run would authorise a real order against the same number. No receipt
is Clone — duplicable proof is not proof.
The same shape covers cancel-replace and editing
(review_amendment → place_reviewed_amendment) and complex orders — OCO, OTOCO,
PAIRS — (review_complex_order → place_reviewed_complex_order). Search, fetch
by id, and cancel are there too, along with the customer-scoped order searches.
A replacement is not atomic at the venue: a fill on the original aborts it. This crate does not paper over that.
Watchlists and quote alerts
Watchlists are the only user-owned mutable resource besides orders — and the only
place this crate can destroy user data. replace_watchlist replaces every
property: the entries sent are the entries that survive.
Quote alerts are set over REST and fire over the account websocket, using the
same QuoteAlert type on both sides.
Backtesting
Server-side strategy backtests, on their own host
(https://backtester.vast.tastyworks.com). Asynchronous: create, poll, read
logs, cancel. The polling is yours — how long to wait is not the library's
decision.
Streaming
Two websockets, and they are different services.
Market data is DXLink, reached with a token from GET /api-quote-tokens. All
eleven event types are routed: quotes, regular and extended-hours trade prints
(TradeETH), Greeks, candles, summaries, time and sale, profiles, underlyings,
theoretical prices and series.
# use ;
# use ;
# use ;
# use InstrumentType;
# async
Two symbol namespaces, and the compiler keeps them apart. The REST API
names an instrument with a Symbol; the feed names it with a DxFeedSymbol.
They are the same string for an equity and differ for futures, options,
cryptocurrencies and warrants: a futures contract the REST API calls /ESU3
streams as /ESU23:XCME. There is no textual rule between the two, and this
crate does not invent one — ask get_streamer_symbol(), or read the
streamer_symbol an instrument already carries, and pass it on unchanged.
Subscribing with the instrument symbol is silent: the venue does not recognise
the target, so it sends nothing, forever, with no error. add_symbols,
add_candles and remove_candles therefore take AsStreamerSymbol, which
only DxFeedSymbol implements. That stops a Symbol, a String or a &str
reaching the feed by accident; it is not validation, since the newtype's field
is public and a wrong string can still be wrapped by hand. The compiler catches
the mix-up, not the typo. CandlePeriod::base_symbol() is the way back from a
bar or a marker, which name their series with the period suffix, to the base
name the subscription methods take.
Historical replay is a phase, not a guess. Each streamer symbol replays its
history as its own snapshot, and the crate turns the feed's IndexedEvent flags
into EventData::SnapshotBegin and EventData::SnapshotEnd — placed after that
replay's last bar and before the first live update, never dropped for a full
queue, and never overtaken by anything newer. Both carry a generation that
increments on each new snapshot and on each reconnect, so an ending still queued
when a connection dropped is identifiable instead of being read as the next
replay's. history_loaded() and await_history() ask the same question
directly, and remove_candles() unsubscribes one finished series while every
other series on the subscription, and every other subscription watching the same
one, keeps running.
Account notifications come over tastytrade's own streamer, authenticated with the access token. It publishes a full object on every change — never a diff — for orders, balances, positions, quote alerts and public watchlists. The fills inside an order's legs are the only place an executed price reaches this crate.
Reconnection. Both sides reconnect under a BackoffPolicy and replay what
was subscribed. state() reports a ConnectionState, and Connected is claimed
only once the subscriptions are restored and the venue has acknowledged them. The
attempt budget resets only on evidence the venue accepted something, so a host
that takes the socket and then refuses the session runs out of attempts instead of
retrying forever.
A subscription's buffer is bounded, so a slow consumer loses events rather than
stalling every other subscription. lagged() makes that observable, and for
candles a dropped bar is recoverable across a reconnect.
The cryptocurrency trading suspension
tastytrade disabled cryptocurrency trading through the API on 2026-06-29, until further notice (release notes). An order with a cryptocurrency leg is refused locally on the placement, dry-run and complex-order paths alike.
Instrument discovery and market data are unaffected.
list_cryptocurrencies, get_cryptocurrency and the DXLink feed all keep
working; only routing is closed. The whole decision is one constant,
CRYPTOCURRENCY_TRADING_ENABLED.
Design decisions you can feel
- Money is
Decimal. Every price, quantity, balance and ratio isrust_decimal::Decimal.f64appears in exactly one place — the DXFeed streaming types, where the feed imposes it — and REST paths never reuse those types even where the field names match. - Secrets never render themselves. The client secret, the refresh and access
tokens, the DXLink token, the AI-search token and the whole customer resource
print as
***or as a field count — not inDebug, not inDisplay, not in a log, not in an error message. Account numbers are redacted from request paths in errors, and a response body is never logged at any level. - A library does not panic. No
unwrap, noexpect, no unchecked indexing on a path reachable from a public method. A local failure isPreconditionand reportsis_retryable()false, because nothing was sent. - An absent field is unknown, never zero. A flag the venue did not send is
None, notfalse. Certification omits fields production sends. Items<T>tolerates one bad row rather than losing a listing of 5,000 — so response enums keep anUnknown(String)arm, because a strict one would make a row disappear silently. Request enums are closed, for the opposite reason.
Examples
Six runnable example crates in the workspace:
| Crate | What it shows |
|---|---|
examples/account-data |
Customer, transactions, trading status, margin, net-liq history |
examples/accounts-status |
Balances, positions, account streaming, frame capture |
examples/instruments |
Equities, futures, options, chains, search |
examples/market-data |
REST snapshots, metrics, sessions, watchlists, quote alerts, backtesting |
examples/orders |
Order search, replace, edit, complex orders |
examples/quote-streaming |
DXLink quotes, greeks, candles |
cp .env.example .env # then fill in the OAuth credentials
TASTYTRADE_USE_DEMO=true cargo run -p instruments --bin test_equities
Anything that mutates state refuses to run outside certification. Anything live-only requires an explicit read-only production opt-in.
Development
make check # the pre-push gate: fmt, clippy, tests, docs — all read-only
make test
make lint
make doc
make coverage
make check must be green before pushing. If the public surface moved,
cargo semver-checks check-release too.
Contact Information
- Author: Joaquín Béjar García
- Email: jb@taunais.com
- Telegram: @joaquin_bejar
- Repository: https://github.com/joaquinbejar/tastytrade
- Crate: https://crates.io/crates/tastytrade
- Documentation: https://docs.rs/tastytrade
Contribution
We welcome contributions to this project! If you would like to contribute, please follow these steps:
- Fork the repository.
- Create a new branch for your feature or bug fix.
- Make your changes and ensure that the project still builds and all tests pass.
- Commit your changes and push your branch to your forked repository.
- Submit a pull request to the main repository.
License
Licensed under the MIT license. See LICENSE.
Related projects
Repositories by the same author that this project depends on, and repositories that depend on it.
Depends on
| Repository | Description |
|---|---|
| DXlink · crates.io | Rust client for the DXLink WebSocket protocol used by tastytrade for real-time market data. |
| pretty-simple-display · crates.io | Derive macros for pretty and simple JSON display formatting. |
Used by
| Repository | Description |
|---|---|
| ChainView | Terminal UI for option chains, Greeks and volatility, real-time and backtest replay. |