dxlink is a Rust client library for the DXLink WebSocket protocol used by tastytrade
for real-time market data. This library provides a clean and type-safe API for connecting
to DXLink servers, subscribing to market events, and processing real-time market data.
- Session lifecycle over the DXLink WebSocket protocol:
SETUP, token authentication, feed channels,FEED_SETUP, subscribe and unsubscribe. - Subscriptions tracked per channel, with
fromTimeandsource, and committed only after the outbound send succeeds. Two channels can hold the same event and symbol independently, and resetting or closing one leaves the others alone. [DXLinkClient::subscriptions] reports what the client believes is live. - Automatic keepalives while the connection is open.
- Market events decoded from the
COMPACTwire format:Quote,Trade,Greeks,Candle,Summary,TimeAndSale,Profile,Underlying,TheoPrice,TradeETHandSeries, each with the full field set the dxFeed schema defines for it. - Both delivery styles: a per-symbol callback and a single event stream.
- Historical data via
from_timeon aCandlesubscription, decoded into OHLC bars. - Typed errors ([
DXLinkError]) with [DXLinkError::is_terminal] to tell a lost connection from one bad message. - Strict decoding: [
try_parse_compact_data] reports a short row, a wrong column type or an unknown event type instead of returning fewer events and letting a consumer read that as a quiet market. - The layout the server negotiates is the one that gets decoded. A feed
may serve fewer fields than were asked for — the dxFeed demo drops
VWAPfromCandle— so columns are read by name rather than by position. A field the server does not send arrives asNaN,0or an empty string rather than as its neighbour's value, and a reordered list is followed rather than refused. - A lost connection is observable: the event stream closes, and
[
DXLinkClient::disconnect_reason] says why. - Opt-in reconnection. Off by default. Install a
[
ReconnectPolicy] with [DXLinkClient::with_reconnect] before connecting and a terminal socket failure is followed by exponential backoff, a fresh handshake, and a replay of every channel, feed configuration and subscription. A rejected token is not retried. [DXLinkClient::connection_states] reports what is happening, dropping the oldest states rather than the newest if a consumer falls behind, so a slow reader can miss the middle of a reconnect but not its outcome.
- [
EventType] declares more variants than the library can decode. OnlyQuote,Trade,Greeks,Candle,Summary,TimeAndSale,Profile,Underlying,TheoPrice,TradeETHandSeriesproduce a [MarketEvent], and configuring or subscribing to any other type is refused rather than accepted into a stream that can never produce.
Rust 1.88. The crate is edition 2024 and uses let-chains, which 1.87 rejects. Raising this floor is a compatibility break and is called out in the release notes; CI builds and tests the full workspace on it so a dependency upgrade cannot raise it silently.
ref: https://raw.githubusercontent.com/dxFeed/dxLink/refs/heads/main/dxlink-specification/asyncapi.yml
Here's a basic example of using the library to connect to a DXLink server and subscribe to market data:
use std::error::Error;
use dxlink::{DXLinkClient, EventType, FeedSubscription, MarketEvent};
use tokio::time::sleep;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
use tracing::info;
// Create a new DXLink client with the API token
// (typically obtained from the tastytrade API)
let token = "your_api_token_here";
let url = "wss://tasty-demo-dxlink-md-ws.dxfeed.com/delayed";
let mut client = DXLinkClient::new(url, token);
// Connect to the DXLink server. This returns the event stream; there is
// exactly one, and asking for it again is an error.
let mut event_stream = client.connect().await?;
// Create a feed channel with AUTO contract type
let channel_id = client.create_feed_channel("AUTO").await?;
// Configure the channel for Quote and Trade events
client.setup_feed(channel_id, &[EventType::Quote, EventType::Trade]).await?;
// Register a callback for specific symbol
client.on_event("SPY", |event| {
info!("Event received for SPY: {:?}", event);
});
// Process events in a separate task
tokio::spawn(async move {
while let Some(event) = event_stream.recv().await {
match &event {
MarketEvent::Quote(quote) => {
info!(
"Quote: {} - Bid: {} x {}, Ask: {} x {}",
quote.event_symbol,
quote.bid_price,
quote.bid_size,
quote.ask_price,
quote.ask_size
);
},
MarketEvent::Trade(trade) => {
info!(
"Trade: {} - Price: {}, Size: {}, Volume: {}",
trade.event_symbol,
trade.price,
trade.size,
trade.day_volume
);
},
_ => info!("Other event type: {:?}", event),
}
}
});
// Subscribe to some symbols
let subscriptions = vec![
FeedSubscription {
event_type: "Quote".to_string(),
symbol: "SPY".to_string(),
from_time: None,
source: None,
},
FeedSubscription {
event_type: "Trade".to_string(),
symbol: "SPY".to_string(),
from_time: None,
source: None,
},
];
client.subscribe(channel_id, subscriptions).await?;
// Keep the connection active for some time
sleep(Duration::from_secs(60)).await;
// Cleanup
client.disconnect().await?;
Ok(())
}DXLink supports subscribing to historical data through Candle events, by specifying the period, type, and a timestamp to fetch from.
The bars come back as [MarketEvent::Candle]:
use dxlink::FeedSubscription;
use std::time::{SystemTime, UNIX_EPOCH};
// Get current timestamp in milliseconds
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
// Timestamp for 24 hours ago
let one_day_ago = now - (24 * 60 * 60 * 1000);
// Subscribe to 5-minute candles for SPY for the last 24 hours
let candle_subscription = FeedSubscription {
event_type: "Candle".to_string(),
symbol: "SPY{=5m}".to_string(), // 5-minute candles
from_time: Some(one_day_ago),
source: None,
};The library uses a custom error type DXLinkError that encompasses
various error cases that can occur when interacting with the DXLink API:
use tracing::{error, info};
use dxlink::{DXLinkClient, DXLinkError};
async fn example_error_handling() {
let mut client = DXLinkClient::new("wss://example.com", "token");
match client.connect().await {
Ok(_) => info!("Connected successfully!"),
Err(DXLinkError::Authentication(e)) => error!("Authentication failed: {}", e),
Err(DXLinkError::Connection(e)) => error!("Connection error: {}", e),
Err(e) => error!("Other error: {}", e),
}
}[EventType] mirrors the event types DXLink itself defines, but only these
are decoded into a [MarketEvent] and delivered:
| Event type | Decoded fields |
|---|---|
Quote |
bidPrice, askPrice, bidSize, askSize |
Trade |
price, size, dayVolume |
Greeks |
delta, gamma, theta, vega, rho, volatility |
Candle |
the full 18-column layout: OHLCV plus VWAP, bid/ask volume, implied volatility, open interest and the snapshot flags |
Summary |
the full 14-column layout: day and previous-day prices, their price types, volume and open interest |
TimeAndSale |
the full 22-column layout: price, size and the surrounding quote plus exchange, sale conditions, aggressor side and the print flags |
Profile |
the full 20-column layout: description, trading status and halt window, price limits, 52-week range and the fundamentals |
Underlying |
the full 13-column layout: implied volatility with its term structure, call and put volume and their ratio |
TheoPrice |
the full 13-column layout: the theoretical price with the underlying price, delta, gamma, dividend and interest it came from |
TradeETH |
the full 15-column layout: the extended-hours print, its session volume and turnover, tick direction and the extended-hours flag |
Series |
the full 15-column layout: per-expiration implied volatility, call and put volume, forward price and the dividend and interest inputs |
Configuring or subscribing to any other variant (Order, SpreadOrder,
…) is refused, rather than accepted into a stream that can
never produce.
This project is licensed under the MIT License. See the LICENSE file for details.
- Clone the repository:
git clone https://github.com/joaquinbejar/DXlink
cd DXlink- Build the project:
make build- Run tests:
make test- Format the code:
make fmt- Run linting:
make lint- Clean the project:
make clean- Run the project:
make run- Fix issues:
make fix- Run pre-push checks:
make pre-push- Generate documentation:
make doc- Publish the package:
make publish- Generate coverage report:
make coverageTo run unit tests:
make testTo run tests with coverage:
make coverageWe 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.
If you have any questions, issues, or would like to provide feedback, please feel free to contact the project maintainer:
Joaquín Béjar García
- Email: jb@taunais.com
- GitHub: joaquinbejar
We appreciate your interest and look forward to your contributions!
License: MIT