Expand description
§tastytrade
A Rust client for the tastytrade brokerage API. Orders placed through it move real money.
This is the API reference. The README is the tour: what the crate covers, how to authenticate, and a worked example per area. What follows is the handful of behaviours a caller has to know before reading any individual method, because they are properties of the whole crate rather than of one call.
§Certification is the default
utils::config::TastyTradeConfig::from_env selects the certification
environment (api.cert.tastyworks.com). Production is a deliberate opt-in:
TASTYTRADE_USE_DEMO=false # production — orders placed here are realOnly 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.
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.
§Authentication is OAuth2, and only OAuth2
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 it.
use tastytrade::TastyTrade;
use tastytrade::utils::config::TastyTradeConfig;
let config = TastyTradeConfig::from_env();
let tasty = TastyTrade::connect(&config).await?;
for account in tasty.accounts().await? {
// Redacted: doc examples get copied, and an account number in a log is
// the thing this crate spends most of its care avoiding.
println!("{}", account.number().redacted());
}Access tokens last about fifteen minutes and every request renews the one in
hand before it expires, so a long-lived client keeps working. A renewal is
never a retry: a POST that may have placed an order is not replayed on a
401.
TastyTrade::connect_with_authorization_code is the third-party grant, for
an application acting on somebody else’s account.
§Nothing that trades happens without a receipt
Placement, replacement, editing and complex orders all go through the same shape: dry-run, read what the venue said, then apply the receipt.
let receipt = account.review_order(order).await?;
for warning in receipt.warnings() {
// Venue prose, written for a person: it can name the account or the
// buying power, so it belongs on a screen rather than in a log.
println!("{warning}");
}
// `accept` refuses when there are warnings. That is not a refusal to
// proceed — it is a refusal to proceed *silently*.
let reviewed = receipt.accept()?;
account.place_reviewed_order(reviewed).await?;A receipt binds the account number and the deployment, because
certification reuses production account numbering — 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.
accounts::Account::place_order still exists for callers managing the
review themselves. It carries no evidence that one happened.
§Money is Decimal
Every price, quantity, balance and ratio is rust_decimal::Decimal.
f64 appears in exactly one place — types::dxfeed, where the streaming
feed imposes it — and REST paths never reuse those types even when the field
names match.
§An absent field is unknown, never zero
A flag the venue did not send is None, not false; a price it did not send
is None, not 0. Certification omits fields production sends, and
“we were not told whether this account is frozen” and “this account is not
frozen” are different facts — only one of them is safe to act on.
§Secrets never render themselves
The client secret, the refresh and access tokens, the DXLink quote token, the
AI-search token and the whole customer resource print as *** or as a field
count. Not in Debug, not in Display, not in a log line, not in an error
message — an error is a string the caller prints wherever they like.
Account numbers are redacted from every request path that reaches an error, and a response body is never logged at any level: an error document from an endpoint this crate does not control can echo a credential.
§A library does not panic
No unwrap, no expect, no unchecked indexing on any path reachable from a
public method. Everything fallible returns TastyTradeError. A local
failure is TastyTradeError::Precondition and reports is_retryable()
false, because nothing was sent.
§Unknown values survive
api::base::Items skips an item it cannot decode rather than failing a
whole listing, so a strict enum on a response would make a row disappear
— silently. The response enums therefore keep an Unknown(String) arm that
round-trips the venue’s text: a new order status, transaction kind or
instrument classification is visible and matchable instead of missing.
Request enums are closed, for the opposite reason: tolerance there would only let a caller send something the venue rejects.
§Cryptocurrency order routing is suspended
tastytrade disabled it on 2026-06-29, until further notice. An order with a
cryptocurrency leg is refused locally on every routing path. Instrument
discovery and market data are unaffected. The whole decision is
prelude::CRYPTOCURRENCY_TRADING_ENABLED, one constant.
§Streaming
Two websockets, and they are different services. Market data is DXLink,
reached with a token from GET /api-quote-tokens; account notifications are
tastytrade’s own streamer, authenticated with the access token. Both
reconnect under a streaming::reconnect::BackoffPolicy and expose
streaming::reconnect::ConnectionState.
Candles are the only route to a price series in this crate, and the only
subscription needing more than a symbol — a candle is addressed by a symbol
carrying its period, AAPL{=5m}.
let mut streamer = tasty.create_quote_streamer().await?;
let mut bars = streamer.create_sub([EventKind::Candle]).await?;
// The streaming name, which is not always the instrument name. For an
// equity the two coincide; for a future they do not, and the feed simply
// never answers the wrong one.
let aapl = tasty
.get_streamer_symbol(&InstrumentType::Equity, &Symbol("AAPL".to_string()))
.await?;
// `from_time` is required, not optional: without one a candle subscription
// replays an unbounded history.
let period = CandlePeriod::minutes(5)?;
bars.add_candles(&[aapl], period, Utc::now() - Duration::days(2))
.await?;
// Each symbol and period replays its history as a snapshot, and the crate
// says when one is over: no flag arithmetic, no snapshot constants.
while let Ok(event) = bars.get_event().await {
match event.data {
EventData::Candle(candle) => {
println!("{}: o {} c {}", event.sym, candle.open, candle.close);
}
EventData::SnapshotEnd(end) => {
println!("{} history in, complete: {}", event.sym, end.lossless);
// Done with it? Stop paying for a live feed nobody reads. The
// marker names the series with its period suffix; this takes it
// without, and the period converts between the two.
if let Some(base) = period.base_symbol(&event.sym) {
bars.remove_candles(&[base], period).await?;
}
break;
}
_ => {}
}
}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 rule to apply here, and this crate does not
invent one — ask
TastyTrade::get_streamer_symbol, or
read the streamer_symbol an instrument already carries, and pass what
comes back unchanged.
Subscribing with the instrument symbol instead is silent: the venue does
not recognise the target, so it sends nothing, forever, with no error. The
subscription methods 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, because 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.
Candle symbols carry their period on the wire, and
dxfeed::CandlePeriod::base_symbol is the way back from a bar or a
marker to the base name the subscription methods take.
Historical replay is a phase, not a guess. A candle subscription’s
history arrives as a snapshot per streamer symbol, and this crate turns the
feed’s flags into two events of its own:
EventData::SnapshotBegin ahead of a
replay’s bars and
EventData::SnapshotEnd after the last
of them, before any live update that follows. Each carries a generation,
which increments on every new snapshot and on every reconnect, so an ending
that was still queued when a connection dropped is recognisable rather than
mistaken for the next replay’s. A marker is never dropped for a full queue,
and nothing newer overtakes it.
QuoteSubscription::history_loaded
and
await_history
answer the same question for a caller who would rather ask than watch, and
remove_candles
drops one finished series without touching the rest of the subscription.
A subscription’s buffer is bounded, so a slow consumer loses events rather
than stalling every other subscription.
streaming::quote_streamer::QuoteSubscription::lagged makes that
observable, and for candles it is recoverable across a reconnect: a dropped
bar stops the resume point advancing, so the next connection asks for it
again. That is also why a replay finishing and a history being complete are
two answers rather than one:
DxfSnapshotEndT::lossless is false
when bars of that generation were shed, by this consumer or by the feed
client above it, so a chart knows it has holes.
The account websocket publishes a full object on every change — never a
diff. The fills inside an order’s legs are the only place an executed price
reaches this crate; no REST endpoint returns one. Anything that is JSON
reaches the caller, including a type nobody here recognises.
§Where to look
prelude re-exports the advertised surface in one import. The endpoint
groups hang off TastyTrade and accounts::Account; the filters that
narrow them are *Filter types taking a api::query::PageRequest.
Re-exports§
pub use api::accounts;pub use api::base::TastyResult;pub use api::client::TastyTrade;
Modules§
- api
- REST surface: authentication, accounts, instruments and option chains. The REST surface of the tastytrade API.
- dxfeed
- Internal DXFeed types to replace external dxfeed dependency This module contains the essential types and constants needed for quote streaming
- oauth
- OAuth2 credentials, grants and token responses.
- prelude
- The commonly used types in one import.
- streaming
- Real-time transports: DXLink quotes and the account websocket.
- utils
- Configuration, logging, bulk downloads and parsing helpers.
Structs§
- ApiError
- Represents an error returned by the Tastytrade API.
- Brief
Position - Represents a brief overview of a position.
- Full
Position - Represents a full position for an account.
- Live
Order Record - Represents a live order record.
- Order
- Represents an order to be placed.
- Order
Builder - Builder for
Order. - Order
Leg - Represents a leg of an order.
- Order
LegBuilder - Builder for
OrderLeg. - Request
Context - Everything known about a failed request that is safe to hand to a caller.
- Symbol
- Represents a trading symbol.
Enums§
- Action
- Represents an order action type.
- DxFeed
Error - Represents errors that can occur during interactions with DxFeed.
- Environment
- Which tastytrade deployment a request was aimed at.
- Exercise
Style - When an option may be exercised.
- Expiration
Type - How an option series expires.
- Instrument
Type - Represents the different types of financial instruments.
- Order
Type - Represents the type of order being placed.
- Price
Effect - Represents the effect of a price on an account.
- Quantity
Direction - Represents the direction of a quantity, such as a trade or position.
- Settlement
Type - When an option settles.
- Tasty
Trade Error - Represents errors that can occur within the Tastytrade API client.
- Time
InForce - Represents the time-in-force instruction for an order.
Traits§
- AsSymbol
- Trait for converting types to
Symbol.