Skip to main content

Crate tastytrade

Crate tastytrade 

Source
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 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.

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.
BriefPosition
Represents a brief overview of a position.
FullPosition
Represents a full position for an account.
LiveOrderRecord
Represents a live order record.
Order
Represents an order to be placed.
OrderBuilder
Builder for Order.
OrderLeg
Represents a leg of an order.
OrderLegBuilder
Builder for OrderLeg.
RequestContext
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.
DxFeedError
Represents errors that can occur during interactions with DxFeed.
Environment
Which tastytrade deployment a request was aimed at.
ExerciseStyle
When an option may be exercised.
ExpirationType
How an option series expires.
InstrumentType
Represents the different types of financial instruments.
OrderType
Represents the type of order being placed.
PriceEffect
Represents the effect of a price on an account.
QuantityDirection
Represents the direction of a quantity, such as a trade or position.
SettlementType
When an option settles.
TastyTradeError
Represents errors that can occur within the Tastytrade API client.
TimeInForce
Represents the time-in-force instruction for an order.

Traits§

AsSymbol
Trait for converting types to Symbol.