1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
/******************************************************************************
Author: Joaquรญn Bรฉjar Garcรญa
Email: jb@taunais.com
Date: 31/8/25
******************************************************************************/
use crate::prelude::{SymbolEntry, TastyTradeConfig, parse_expiration_date};
use crate::{InstrumentType, TastyTrade};
use chrono::{DateTime, Utc};
use std::collections::HashSet;
use tracing::{error, info};
/// Downloads all FutureOption and EquityOption symbols from TastyTrade
pub async fn download_options_symbols() -> Result<Vec<SymbolEntry>, Box<dyn std::error::Error>> {
// Load configuration from environment
let config = TastyTradeConfig::new();
// Check if we have valid credentials
if !config.has_valid_credentials() {
error!(
"โ No valid credentials found. Please set TASTYTRADE_USERNAME and TASTYTRADE_PASSWORD environment variables."
);
return Err("Missing credentials".into());
}
info!("๐ Logging into TastyTrade...");
let tasty = TastyTrade::login(&config).await?;
info!("โ
Successfully logged in!");
let mut all_symbols = Vec::new();
let now = Utc::now();
// Download EquityOptions
info!("๐ Downloading EquityOption symbols...");
match download_equity_options(&tasty, now).await {
Ok(mut equity_options) => {
info!(
"โ
Downloaded {} EquityOption symbols",
equity_options.len()
);
all_symbols.append(&mut equity_options);
}
Err(e) => {
error!("โ ๏ธ Error downloading EquityOptions: {}", e);
}
}
// Download FutureOptions
info!("๐ฎ Downloading FutureOption symbols...");
match download_future_options(&tasty, now).await {
Ok(mut future_options) => {
info!(
"โ
Downloaded {} FutureOption symbols",
future_options.len()
);
all_symbols.append(&mut future_options);
}
Err(e) => {
error!("โ ๏ธ Error downloading FutureOptions: {}", e);
}
}
// Remove duplicates using HashSet
let unique_symbols: HashSet<SymbolEntry> = all_symbols.into_iter().collect();
let final_symbols: Vec<SymbolEntry> = unique_symbols.into_iter().collect();
info!(
"๐ฏ Total unique symbols downloaded: {}",
final_symbols.len()
);
Ok(final_symbols)
}
/// Downloads EquityOption symbols from TastyTrade
async fn download_equity_options(
tasty: &TastyTrade,
last_update: DateTime<Utc>,
) -> Result<Vec<SymbolEntry>, Box<dyn std::error::Error>> {
let mut symbols = Vec::new();
// Try different approaches to get equity symbols
info!(" ๐ Getting equity symbols using multiple approaches...");
let mut all_equities = Vec::new();
// Approach 1: Try to get active equities with pagination
info!(" ๐ Trying list_active_equities...");
let max_pages = 5; // Limit to avoid infinite loops
for page in 0..max_pages {
match tasty.list_active_equities(page).await {
Ok(paginated_equities) => {
let current_count = paginated_equities.items.len();
info!(" ๐ Page {}: {} items found", page, current_count);
// Check pagination info first
let pagination = &paginated_equities.pagination;
// Debug: Print full response structure
info!(" ๐ DEBUG - Full response for page {}:", page);
info!(" ๐ Items count: {}", current_count);
// Print ALL items in this page
for (i, item) in paginated_equities.items.iter().enumerate() {
info!(
" ๐ Item {}: symbol={}, id={}, active={}, description={}",
i, item.symbol.0, item.id, item.active, item.description
);
}
if current_count == 0 {
info!(
" ๐ โ ๏ธ PAGE {} IS EMPTY - but API says there are {} total items",
page, pagination.total_items
);
}
info!(
" ๐ Pagination: page {}/{}, total items: {}",
pagination.page_offset, pagination.total_pages, pagination.total_items
);
info!(
" ๐ DEBUG - Pagination details: per_page={}, item_offset={}, current_item_count={}",
pagination.per_page, pagination.item_offset, pagination.current_item_count
);
if current_count > 0 {
all_equities.extend(paginated_equities.items);
}
// Break if we've reached the last page
if pagination.page_offset + 1 >= pagination.total_pages {
break;
}
// If we have total_items but no items on this page, continue to next page
if current_count == 0 && pagination.total_items > 0 {
info!(
" ๐ Empty page but {} total items exist, continuing...",
pagination.total_items
);
continue;
}
// If no items and no total items, we're done
if current_count == 0 && pagination.total_items == 0 {
break;
}
}
Err(e) => {
error!("Error fetching active equities at page {}: {}", page, e);
break;
}
}
}
// If we didn't get any equities, there's a problem that needs investigation
if all_equities.is_empty() {
error!(" โ No equity instruments found via list_active_equities API");
error!(" ๐ This indicates a potential API issue or authentication problem");
return Err("No equity instruments found - check API connectivity and credentials".into());
}
info!(" ๐ Found {} total equity instruments", all_equities.len());
// Process options for each equity (limit to avoid overwhelming API)
let max_equities = std::env::var("MAX_EQUITIES")
.unwrap_or_else(|_| "100".to_string())
.parse::<usize>()
.unwrap_or(100);
let equities_to_process = if all_equities.len() > max_equities {
info!(
" โ ๏ธ Limiting to {} equities (set MAX_EQUITIES env var to change)",
max_equities
);
&all_equities[..max_equities]
} else {
&all_equities
};
for equity in equities_to_process {
info!(" ๐ Processing options for {}", equity.symbol.0);
// Get nested option chains for this equity
match tasty.list_nested_option_chains(equity.symbol.clone()).await {
Ok(option_chains) => {
for chain in option_chains {
// Process each expiration in the chain
for expiration in &chain.expirations {
// Parse expiration date
let expiry =
parse_expiration_date(&expiration.expiration_date, last_update);
// Process each strike in the expiration
for strike in &expiration.strikes {
// Add call option
symbols.push(SymbolEntry {
symbol: strike.call.0.clone(),
epic: strike.call.0.clone(), // Using symbol as epic for TastyTrade
name: format!(
"{} Call ${} {}",
chain.underlying_symbol.0,
strike.strike_price,
expiration.expiration_date
),
instrument_type: InstrumentType::EquityOption,
exchange: "TASTYTRADE".to_string(),
expiry,
last_update,
});
// Add put option
symbols.push(SymbolEntry {
symbol: strike.put.0.clone(),
epic: strike.put.0.clone(), // Using symbol as epic for TastyTrade
name: format!(
"{} Put ${} {}",
chain.underlying_symbol.0,
strike.strike_price,
expiration.expiration_date
),
instrument_type: InstrumentType::EquityOption,
exchange: "TASTYTRADE".to_string(),
expiry,
last_update,
});
}
}
}
}
Err(e) => {
error!(
" โ ๏ธ Error getting option chain for {}: {}",
equity.symbol.0, e
);
}
}
}
Ok(symbols)
}
/// Downloads FutureOption symbols from TastyTrade
async fn download_future_options(
tasty: &TastyTrade,
last_update: DateTime<Utc>,
) -> Result<Vec<SymbolEntry>, Box<dyn std::error::Error>> {
let mut symbols = Vec::new();
// Get ALL future products
info!(" ๐ Getting all future products...");
let future_products = tasty.list_future_products().await?;
info!(" ๐ Found {} total future products", future_products.len());
// Process all future products (with optional limit via env var)
let max_products = std::env::var("MAX_FUTURE_PRODUCTS")
.unwrap_or_else(|_| "50".to_string())
.parse::<usize>()
.unwrap_or(50);
let products_to_process = if future_products.len() > max_products {
info!(
" โ ๏ธ Limiting to {} future products (set MAX_FUTURE_PRODUCTS env var to change)",
max_products
);
&future_products[..max_products]
} else {
&future_products
};
// Products that typically don't have option chains
let products_without_options = [
"GE", // Eurodollar
"ZQ", // 30 Day Fed Fund
"ZT", // 2-Year Note
"ZF", // 5-Year Note
"ZN", // 10-Year Note
"ZB", // 30-Year Bond
"UB",
];
for product in products_to_process {
info!(
" ๐ฎ Processing future options for product: {} ({})",
product.code, product.description
);
// Skip products that typically don't have option chains
if products_without_options.contains(&product.code.as_str()) {
info!(
" ๐ {} ({}) typically has no option chains - skipping",
product.code, product.description
);
continue;
}
// Get nested option chains for this future product
match tasty.list_nested_futures_option_chains(&product.code).await {
Ok(option_chains) => {
if option_chains.is_empty() {
info!(
" ๐ญ No option chains found for {} ({})",
product.code, product.description
);
continue;
}
info!(
" โ
Found {} option chains for {}",
option_chains.len(),
product.code
);
for chain in option_chains {
// Process each expiration in the chain
for expiration in &chain.expirations {
// Parse expiration date
let expiry =
parse_expiration_date(&expiration.expiration_date, last_update);
// Process each strike in the expiration
for strike in &expiration.strikes {
// Add call option
symbols.push(SymbolEntry {
symbol: strike.call.0.clone(),
epic: strike.call.0.clone(), // Using symbol as epic for TastyTrade
name: format!(
"{} Future Call ${} {}",
chain.underlying_symbol.0,
strike.strike_price,
expiration.expiration_date
),
instrument_type: InstrumentType::FutureOption,
exchange: "TASTYTRADE".to_string(),
expiry,
last_update,
});
// Add put option
symbols.push(SymbolEntry {
symbol: strike.put.0.clone(),
epic: strike.put.0.clone(), // Using symbol as epic for TastyTrade
name: format!(
"{} Future Put ${} {}",
chain.underlying_symbol.0,
strike.strike_price,
expiration.expiration_date
),
instrument_type: InstrumentType::FutureOption,
exchange: "TASTYTRADE".to_string(),
expiry,
last_update,
});
}
}
}
}
Err(e) => {
// Check if it's a decoding error specifically
let error_msg = format!("{}", e);
if error_msg.contains("error decoding response body") {
info!(
" ๐ {} ({}) has no option chains or unsupported format - skipping",
product.code, product.description
);
} else {
error!(
" โ ๏ธ API error for {} ({}): {}",
product.code, product.description, e
);
}
}
}
}
Ok(symbols)
}