tastyworks/
lib.rs

1//! Unofficial tastyworks/tastytrade API for Rust. Requires [API access to be enabled](https://support.tastytrade.com/support/s/solutions/articles/43000700385) for your account.
2//!
3//! ## Example
4//!
5//! ```rust
6//! use tastyworks::Session;
7//! use num_traits::ToPrimitive;
8//!
9//! // Requests made by the API are asynchronous, so you must use a runtime such as `tokio`.
10//! #[tokio::main]
11//! async fn main() {
12//!   let login = "username"; // or email
13//!   let password = "password";
14//!   let otp = Some("123456"); // 2FA code, may be None::<String>
15//!   let session = Session::from_credentials(login, password, otp)
16//!       .await.expect("Failed to login");
17//!
18//!   let accounts = tastyworks::accounts(&session)
19//!       .await.expect("Failed to fetch accounts");
20//!   let account = accounts.first().expect("No accounts found");
21//!
22//!   let positions = tastyworks::positions(account, &session)
23//!       .await.expect("Failed to fetch positions");
24//!
25//!   println!("Your active positions:");
26//!   for position in &positions {
27//!       let signed_quantity = position.signed_quantity();
28//!
29//!       // Quantities in the API that could potentially be decimal values are stored as
30//!       // `num_rational::Rational64`. To convert these to floats include the `num-traits` crate
31//!       // in your project and use the `ToPrimitive` trait. To convert these to integers no
32//!       // additional crate is required.
33//!       println!(
34//!           "{:>10} x {}",
35//!           if signed_quantity.is_integer() {
36//!               signed_quantity.to_integer().to_string()
37//!           } else {
38//!               signed_quantity.to_f64().unwrap().to_string()
39//!           },
40//!           position.symbol
41//!       );
42//!   }
43//! }
44//! ```
45
46use chrono::{DateTime, TimeZone, Utc};
47use futures::{stream, StreamExt};
48use itertools::Itertools;
49
50pub mod api;
51pub mod common;
52pub mod csv;
53pub mod errors;
54pub mod request;
55pub mod session;
56pub mod streamer;
57pub mod symbol;
58
59use crate::errors::*;
60pub use crate::{api::*, request::*, session::Session};
61
62const MAX_SYMBOL_SUMMARY_BATCH_SIZE: usize = 500;
63const PARALLEL_REQUESTS: usize = 10;
64
65pub async fn accounts(session: &Session) -> Result<Vec<accounts::Account>, ApiError> {
66    let url = "customers/me/accounts";
67    let response: api::Response<accounts::Response> =
68        deserialize_response(request(url, "", session).await?).await?;
69    Ok(response
70        .data
71        .items
72        .into_iter()
73        .map(|item| item.account)
74        .collect())
75}
76
77pub async fn watchlists(session: &Session) -> Result<Vec<watchlists::Item>, ApiError> {
78    let url = "watchlists";
79    let response: api::Response<watchlists::Response> =
80        deserialize_response(request(url, "", session).await?).await?;
81    Ok(response.data.items)
82}
83
84pub async fn public_watchlists(session: &Session) -> Result<Vec<watchlists::Item>, ApiError> {
85    let url = "public-watchlists";
86    let response: api::Response<watchlists::Response> =
87        deserialize_response(request(url, "", session).await?).await?;
88    Ok(response.data.items)
89}
90
91pub async fn balances(
92    account: &accounts::Account,
93    session: &Session,
94) -> Result<balances::Data, ApiError> {
95    let url = format!("accounts/{}/balances", account.account_number);
96    let response: api::Response<balances::Data> =
97        deserialize_response(request(&url, "", session).await?).await?;
98    Ok(response.data)
99}
100
101pub async fn positions(
102    account: &accounts::Account,
103    session: &Session,
104) -> Result<Vec<positions::Item>, ApiError> {
105    let url = format!("accounts/{}/positions", account.account_number);
106    let response: api::Response<positions::Response> =
107        deserialize_response(request(&url, "", session).await?).await?;
108    Ok(response.data.items)
109}
110
111pub async fn transactions<Tz: TimeZone>(
112    account: &accounts::Account,
113    start_date: DateTime<Tz>,
114    end_date: DateTime<Tz>,
115    prev_pagination: Option<Pagination>,
116    session: &Session,
117) -> Result<Option<(Vec<transactions::Item>, Option<Pagination>)>, ApiError> {
118    let page_offset = if let Some(api::Pagination {
119        page_offset,
120        total_pages,
121        ..
122    }) = prev_pagination
123    {
124        if page_offset + 1 >= total_pages {
125            return Ok(None);
126        }
127        page_offset + 1
128    } else {
129        0
130    };
131
132    let url = format!("accounts/{}/transactions", account.account_number);
133    let parameters = format!(
134        "start-date={}&end-date={}&page-offset={}",
135        start_date.with_timezone(&Utc),
136        end_date.with_timezone(&Utc),
137        page_offset
138    );
139    let response: api::Response<transactions::Response> =
140        deserialize_response(request(&url, &parameters, session).await?).await?;
141
142    Ok(Some((response.data.items, response.pagination)))
143}
144
145pub async fn market_metrics(
146    symbols: &[String],
147    session: &Session,
148) -> Result<Vec<market_metrics::Item>, ApiError> {
149    let results = stream::iter(symbols.chunks(MAX_SYMBOL_SUMMARY_BATCH_SIZE).map(
150        |batch| async move {
151            let symbols = batch.iter().cloned().join(",");
152
153            let url_path = "market-metrics";
154            let params_string = &format!("symbols={}", symbols);
155            let response: Result<api::Response<market_metrics::Response>, ApiError> =
156                deserialize_response(request(url_path, params_string, session).await?).await;
157
158            response
159        },
160    ))
161    .buffered(PARALLEL_REQUESTS)
162    .collect::<Vec<_>>()
163    .await;
164
165    let mut json = vec![];
166    for result in results.into_iter() {
167        json.append(&mut result?.data.items);
168    }
169
170    Ok(json)
171}
172
173pub async fn option_chains(
174    symbol: &str,
175    session: &Session,
176) -> Result<Vec<option_chains::Item>, ApiError> {
177    let url = format!("option-chains/{}/nested", symbol);
178    let response: api::Response<option_chains::Response> =
179        deserialize_response(request(&url, "", session).await?).await?;
180    Ok(response.data.items)
181}