1use chrono::{DateTime, Utc};
14use serde::{Deserialize, Serialize};
15
16use crate::Client;
17use crate::Error;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "PascalCase")]
43pub struct Bar {
44 pub high: String,
46 pub low: String,
48 pub open: String,
50 pub close: String,
52 pub time_stamp: String,
54 pub total_volume: String,
56 #[serde(default)]
58 pub down_ticks: Option<serde_json::Value>,
59 #[serde(default)]
61 pub down_volume: Option<serde_json::Value>,
62 #[serde(default)]
64 pub open_interest: Option<serde_json::Value>,
65 #[serde(default)]
67 pub total_ticks: Option<serde_json::Value>,
68 #[serde(default)]
70 pub unchanged_ticks: Option<serde_json::Value>,
71 #[serde(default)]
73 pub unchanged_volume: Option<serde_json::Value>,
74 #[serde(default)]
76 pub up_ticks: Option<serde_json::Value>,
77 #[serde(default)]
79 pub up_volume: Option<serde_json::Value>,
80 #[serde(default)]
82 pub is_realtime: Option<bool>,
83 #[serde(default)]
85 pub is_end_of_history: Option<bool>,
86 #[serde(default)]
88 pub epoch: Option<u64>,
89 #[serde(default)]
91 pub bar_status: Option<String>,
92}
93
94impl Bar {
95 pub fn ohlcv(&self) -> Option<(f64, f64, f64, f64, u64)> {
99 Some((
100 self.open.parse().ok()?,
101 self.high.parse().ok()?,
102 self.low.parse().ok()?,
103 self.close.parse().ok()?,
104 self.total_volume.parse().ok()?,
105 ))
106 }
107
108 pub fn timestamp(&self) -> Option<DateTime<Utc>> {
112 DateTime::parse_from_rfc3339(&self.time_stamp)
113 .ok()
114 .map(|dt| dt.with_timezone(&Utc))
115 }
116}
117
118#[derive(Debug, Deserialize)]
120#[serde(rename_all = "PascalCase")]
121pub struct BarChartResponse {
122 pub bars: Vec<Bar>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(rename_all = "PascalCase")]
130pub struct Quote {
131 pub symbol: String,
133 pub last: String,
135 pub ask: String,
137 pub bid: String,
139 pub volume: String,
141 #[serde(default)]
143 pub close: Option<String>,
144 #[serde(default)]
146 pub high: Option<String>,
147 #[serde(default)]
149 pub low: Option<String>,
150 #[serde(default)]
152 pub open: Option<String>,
153 #[serde(default)]
155 pub net_change: Option<String>,
156 #[serde(default)]
158 pub net_change_pct: Option<String>,
159 #[serde(rename = "TradeTime", default)]
161 pub trade_time: Option<String>,
162}
163
164#[derive(Debug, Deserialize)]
166#[serde(rename_all = "PascalCase")]
167pub struct QuoteResponse {
168 pub quotes: Vec<Quote>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "PascalCase")]
176pub struct SymbolInfo {
177 pub symbol: String,
179 pub description: Option<String>,
181 pub exchange: Option<String>,
183 pub category: Option<String>,
185 #[serde(default)]
187 pub currency: Option<String>,
188 #[serde(default)]
190 pub point_value: Option<String>,
191 #[serde(default)]
193 pub min_move: Option<String>,
194}
195
196#[derive(Debug, Deserialize)]
198#[serde(rename_all = "PascalCase")]
199struct SymbolResponse {
200 definitions: Vec<SymbolInfo>,
201}
202
203pub struct BarChartQuery {
219 pub symbol: String,
221 pub interval: String,
223 pub unit: String,
225 pub bars_back: Option<u32>,
227 pub first_date: Option<String>,
229 pub last_date: Option<String>,
231 pub session_template: Option<String>,
233}
234
235impl BarChartQuery {
236 pub fn minute_bars(symbol: impl Into<String>, bars_back: u32) -> Self {
238 Self {
239 symbol: symbol.into(),
240 interval: "1".to_string(),
241 unit: "Minute".to_string(),
242 bars_back: Some(bars_back),
243 first_date: None,
244 last_date: None,
245 session_template: None,
246 }
247 }
248
249 pub fn daily_bars(symbol: impl Into<String>, bars_back: u32) -> Self {
251 Self {
252 symbol: symbol.into(),
253 interval: "1".to_string(),
254 unit: "Daily".to_string(),
255 bars_back: Some(bars_back),
256 first_date: None,
257 last_date: None,
258 session_template: None,
259 }
260 }
261
262 pub fn with_dates(mut self, first_date: &str, last_date: &str) -> Self {
264 self.first_date = Some(first_date.to_string());
265 self.last_date = Some(last_date.to_string());
266 self.bars_back = None;
267 self
268 }
269
270 fn to_query_params(&self) -> Vec<(&str, String)> {
271 let mut params = vec![
272 ("interval", self.interval.clone()),
273 ("unit", self.unit.clone()),
274 ];
275 if let Some(bars_back) = self.bars_back {
276 params.push(("barsBack", bars_back.to_string()));
277 }
278 if let Some(first_date) = &self.first_date {
279 params.push(("firstDate", first_date.clone()));
280 }
281 if let Some(last_date) = &self.last_date {
282 params.push(("lastDate", last_date.clone()));
283 }
284 if let Some(session) = &self.session_template {
285 params.push(("sessionTemplate", session.clone()));
286 }
287 params
288 }
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293#[serde(rename_all = "PascalCase")]
294pub struct CryptoPair {
295 pub name: String,
297}
298
299#[derive(Debug, Deserialize)]
301#[serde(rename_all = "PascalCase")]
302struct CryptoPairsResponse {
303 crypto_pairs: Vec<String>,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(rename_all = "PascalCase")]
311pub struct OptionExpiration {
312 pub date: String,
314 #[serde(default, rename = "Type")]
316 pub expiration_type: Option<String>,
317}
318
319#[derive(Debug, Deserialize)]
321#[serde(rename_all = "PascalCase")]
322struct OptionExpirationsResponse {
323 expirations: Vec<OptionExpiration>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
330#[serde(rename_all = "PascalCase")]
331pub struct OptionStrike {
332 pub strike_price: String,
334}
335
336#[derive(Debug, Deserialize)]
338#[serde(rename_all = "PascalCase")]
339struct OptionStrikesResponse {
340 strikes: Vec<OptionStrike>,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
347#[serde(rename_all = "PascalCase")]
348pub struct SpreadType {
349 pub name: String,
351 #[serde(default)]
353 pub description: Option<String>,
354}
355
356#[derive(Debug, Deserialize)]
358#[serde(rename_all = "PascalCase")]
359struct SpreadTypesResponse {
360 spread_types: Vec<SpreadType>,
361}
362
363#[derive(Debug, Serialize)]
367#[serde(rename_all = "PascalCase")]
368pub struct RiskRewardRequest {
369 pub symbol: String,
371 pub trade_action: String,
373 pub quantity: String,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub limit_price: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub stop_price: Option<String>,
381}
382
383#[derive(Debug, Clone, Deserialize)]
385#[serde(rename_all = "PascalCase")]
386pub struct RiskRewardResponse {
387 #[serde(default)]
389 pub max_reward: Option<String>,
390 #[serde(default)]
392 pub max_risk: Option<String>,
393 #[serde(default)]
395 pub break_even: Option<String>,
396}
397
398pub trait ParseNumeric {
414 fn parse_f64(&self) -> Option<f64>;
416 fn parse_u64(&self) -> Option<u64>;
418}
419
420impl ParseNumeric for str {
421 fn parse_f64(&self) -> Option<f64> {
422 self.parse().ok()
423 }
424
425 fn parse_u64(&self) -> Option<u64> {
426 self.parse().ok()
427 }
428}
429
430impl ParseNumeric for String {
431 fn parse_f64(&self) -> Option<f64> {
432 self.parse().ok()
433 }
434
435 fn parse_u64(&self) -> Option<u64> {
436 self.parse().ok()
437 }
438}
439
440impl ParseNumeric for Option<String> {
441 fn parse_f64(&self) -> Option<f64> {
442 self.as_deref()?.parse().ok()
443 }
444
445 fn parse_u64(&self) -> Option<u64> {
446 self.as_deref()?.parse().ok()
447 }
448}
449
450impl Client {
451 pub async fn get_bars(&mut self, query: &BarChartQuery) -> Result<Vec<Bar>, Error> {
457 let path = format!("/v3/marketdata/barcharts/{}", query.symbol);
458 let headers = self.auth_headers().await?;
459 let url = format!("{}{}", self.base_url(), &path);
460
461 let mut req = self.http.get(&url).headers(headers);
462 for (key, value) in query.to_query_params() {
463 req = req.query(&[(key, &value)]);
464 }
465
466 let resp = req.send().await?;
467 if !resp.status().is_success() {
468 let status = resp.status().as_u16();
469 let body = resp.text().await.unwrap_or_default();
470 return Err(Error::Api {
471 status,
472 message: body,
473 });
474 }
475
476 let chart_resp: BarChartResponse = resp.json().await?;
477 Ok(chart_resp.bars)
478 }
479
480 pub async fn get_quotes(&mut self, symbols: &[&str]) -> Result<Vec<Quote>, Error> {
484 let symbols_str = symbols.join(",");
485 let path = format!("/v3/marketdata/quotes/{}", symbols_str);
486 let resp = self.get(&path).await?;
487 let quote_resp: QuoteResponse = resp.json().await?;
488 Ok(quote_resp.quotes)
489 }
490
491 pub async fn get_symbol_info(&mut self, symbols: &[&str]) -> Result<Vec<SymbolInfo>, Error> {
493 let symbols_str = symbols.join(",");
494 let path = format!("/v3/marketdata/symbols/{}", symbols_str);
495 let resp = self.get(&path).await?;
496 let symbol_resp: SymbolResponse = resp.json().await?;
497 Ok(symbol_resp.definitions)
498 }
499
500 pub async fn get_crypto_pairs(&mut self) -> Result<Vec<String>, Error> {
502 let resp = self
503 .get("/v3/marketdata/symbollists/cryptopairs/symbolnames")
504 .await?;
505 let data: CryptoPairsResponse = resp.json().await?;
506 Ok(data.crypto_pairs)
507 }
508
509 pub async fn get_option_expirations(
511 &mut self,
512 underlying: &str,
513 ) -> Result<Vec<OptionExpiration>, Error> {
514 let path = format!("/v3/marketdata/options/expirations/{}", underlying);
515 let resp = self.get(&path).await?;
516 let data: OptionExpirationsResponse = resp.json().await?;
517 Ok(data.expirations)
518 }
519
520 pub async fn get_option_strikes(
522 &mut self,
523 underlying: &str,
524 ) -> Result<Vec<OptionStrike>, Error> {
525 let path = format!("/v3/marketdata/options/strikes/{}", underlying);
526 let resp = self.get(&path).await?;
527 let data: OptionStrikesResponse = resp.json().await?;
528 Ok(data.strikes)
529 }
530
531 pub async fn get_option_spread_types(&mut self) -> Result<Vec<SpreadType>, Error> {
533 let resp = self.get("/v3/marketdata/options/spreadtypes").await?;
534 let data: SpreadTypesResponse = resp.json().await?;
535 Ok(data.spread_types)
536 }
537
538 pub async fn get_option_risk_reward(
540 &mut self,
541 request: &RiskRewardRequest,
542 ) -> Result<RiskRewardResponse, Error> {
543 let resp = self
544 .post("/v3/marketdata/options/riskreward", request)
545 .await?;
546 Ok(resp.json().await?)
547 }
548}