use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::Client;
use crate::Error;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Bar {
pub high: String,
pub low: String,
pub open: String,
pub close: String,
pub time_stamp: String,
pub total_volume: String,
#[serde(default)]
pub down_ticks: Option<serde_json::Value>,
#[serde(default)]
pub down_volume: Option<serde_json::Value>,
#[serde(default)]
pub open_interest: Option<serde_json::Value>,
#[serde(default)]
pub total_ticks: Option<serde_json::Value>,
#[serde(default)]
pub unchanged_ticks: Option<serde_json::Value>,
#[serde(default)]
pub unchanged_volume: Option<serde_json::Value>,
#[serde(default)]
pub up_ticks: Option<serde_json::Value>,
#[serde(default)]
pub up_volume: Option<serde_json::Value>,
#[serde(default)]
pub is_realtime: Option<bool>,
#[serde(default)]
pub is_end_of_history: Option<bool>,
#[serde(default)]
pub epoch: Option<u64>,
#[serde(default)]
pub bar_status: Option<String>,
}
impl Bar {
pub fn ohlcv(&self) -> Option<(f64, f64, f64, f64, u64)> {
Some((
self.open.parse().ok()?,
self.high.parse().ok()?,
self.low.parse().ok()?,
self.close.parse().ok()?,
self.total_volume.parse().ok()?,
))
}
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
DateTime::parse_from_rfc3339(&self.time_stamp)
.ok()
.map(|dt| dt.with_timezone(&Utc))
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct BarChartResponse {
pub bars: Vec<Bar>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Quote {
pub symbol: String,
pub last: String,
pub ask: String,
pub bid: String,
pub volume: String,
#[serde(default)]
pub close: Option<String>,
#[serde(default)]
pub high: Option<String>,
#[serde(default)]
pub low: Option<String>,
#[serde(default)]
pub open: Option<String>,
#[serde(default)]
pub net_change: Option<String>,
#[serde(default)]
pub net_change_pct: Option<String>,
#[serde(rename = "TradeTime", default)]
pub trade_time: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct QuoteResponse {
pub quotes: Vec<Quote>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SymbolInfo {
pub symbol: String,
pub description: Option<String>,
pub exchange: Option<String>,
pub category: Option<String>,
#[serde(default)]
pub currency: Option<String>,
#[serde(default)]
pub point_value: Option<String>,
#[serde(default)]
pub min_move: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct SymbolResponse {
definitions: Vec<SymbolInfo>,
}
pub struct BarChartQuery {
pub symbol: String,
pub interval: String,
pub unit: String,
pub bars_back: Option<u32>,
pub first_date: Option<String>,
pub last_date: Option<String>,
pub session_template: Option<String>,
}
impl BarChartQuery {
pub fn minute_bars(symbol: impl Into<String>, bars_back: u32) -> Self {
Self {
symbol: symbol.into(),
interval: "1".to_string(),
unit: "Minute".to_string(),
bars_back: Some(bars_back),
first_date: None,
last_date: None,
session_template: None,
}
}
pub fn daily_bars(symbol: impl Into<String>, bars_back: u32) -> Self {
Self {
symbol: symbol.into(),
interval: "1".to_string(),
unit: "Daily".to_string(),
bars_back: Some(bars_back),
first_date: None,
last_date: None,
session_template: None,
}
}
pub fn with_dates(mut self, first_date: &str, last_date: &str) -> Self {
self.first_date = Some(first_date.to_string());
self.last_date = Some(last_date.to_string());
self.bars_back = None;
self
}
fn to_query_params(&self) -> Vec<(&str, String)> {
let mut params = vec![
("interval", self.interval.clone()),
("unit", self.unit.clone()),
];
if let Some(bars_back) = self.bars_back {
params.push(("barsBack", bars_back.to_string()));
}
if let Some(first_date) = &self.first_date {
params.push(("firstDate", first_date.clone()));
}
if let Some(last_date) = &self.last_date {
params.push(("lastDate", last_date.clone()));
}
if let Some(session) = &self.session_template {
params.push(("sessionTemplate", session.clone()));
}
params
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct CryptoPair {
pub name: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct CryptoPairsResponse {
crypto_pairs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct OptionExpiration {
pub date: String,
#[serde(default, rename = "Type")]
pub expiration_type: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct OptionExpirationsResponse {
expirations: Vec<OptionExpiration>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct OptionStrike {
pub strike_price: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct OptionStrikesResponse {
strikes: Vec<OptionStrike>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SpreadType {
pub name: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct SpreadTypesResponse {
spread_types: Vec<SpreadType>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct RiskRewardRequest {
pub symbol: String,
pub trade_action: String,
pub quantity: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit_price: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_price: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RiskRewardResponse {
#[serde(default)]
pub max_reward: Option<String>,
#[serde(default)]
pub max_risk: Option<String>,
#[serde(default)]
pub break_even: Option<String>,
}
pub trait ParseNumeric {
fn parse_f64(&self) -> Option<f64>;
fn parse_u64(&self) -> Option<u64>;
}
impl ParseNumeric for str {
fn parse_f64(&self) -> Option<f64> {
self.parse().ok()
}
fn parse_u64(&self) -> Option<u64> {
self.parse().ok()
}
}
impl ParseNumeric for String {
fn parse_f64(&self) -> Option<f64> {
self.parse().ok()
}
fn parse_u64(&self) -> Option<u64> {
self.parse().ok()
}
}
impl ParseNumeric for Option<String> {
fn parse_f64(&self) -> Option<f64> {
self.as_deref()?.parse().ok()
}
fn parse_u64(&self) -> Option<u64> {
self.as_deref()?.parse().ok()
}
}
impl Client {
pub async fn get_bars(&mut self, query: &BarChartQuery) -> Result<Vec<Bar>, Error> {
let path = format!("/v3/marketdata/barcharts/{}", query.symbol);
let headers = self.auth_headers().await?;
let url = format!("{}{}", self.base_url(), &path);
let mut req = self.http.get(&url).headers(headers);
for (key, value) in query.to_query_params() {
req = req.query(&[(key, &value)]);
}
let resp = req.send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(Error::Api {
status,
message: body,
});
}
let chart_resp: BarChartResponse = resp.json().await?;
Ok(chart_resp.bars)
}
pub async fn get_quotes(&mut self, symbols: &[&str]) -> Result<Vec<Quote>, Error> {
let symbols_str = symbols.join(",");
let path = format!("/v3/marketdata/quotes/{}", symbols_str);
let resp = self.get(&path).await?;
let quote_resp: QuoteResponse = resp.json().await?;
Ok(quote_resp.quotes)
}
pub async fn get_symbol_info(&mut self, symbols: &[&str]) -> Result<Vec<SymbolInfo>, Error> {
let symbols_str = symbols.join(",");
let path = format!("/v3/marketdata/symbols/{}", symbols_str);
let resp = self.get(&path).await?;
let symbol_resp: SymbolResponse = resp.json().await?;
Ok(symbol_resp.definitions)
}
pub async fn get_crypto_pairs(&mut self) -> Result<Vec<String>, Error> {
let resp = self
.get("/v3/marketdata/symbollists/cryptopairs/symbolnames")
.await?;
let data: CryptoPairsResponse = resp.json().await?;
Ok(data.crypto_pairs)
}
pub async fn get_option_expirations(
&mut self,
underlying: &str,
) -> Result<Vec<OptionExpiration>, Error> {
let path = format!("/v3/marketdata/options/expirations/{}", underlying);
let resp = self.get(&path).await?;
let data: OptionExpirationsResponse = resp.json().await?;
Ok(data.expirations)
}
pub async fn get_option_strikes(
&mut self,
underlying: &str,
) -> Result<Vec<OptionStrike>, Error> {
let path = format!("/v3/marketdata/options/strikes/{}", underlying);
let resp = self.get(&path).await?;
let data: OptionStrikesResponse = resp.json().await?;
Ok(data.strikes)
}
pub async fn get_option_spread_types(&mut self) -> Result<Vec<SpreadType>, Error> {
let resp = self.get("/v3/marketdata/options/spreadtypes").await?;
let data: SpreadTypesResponse = resp.json().await?;
Ok(data.spread_types)
}
pub async fn get_option_risk_reward(
&mut self,
request: &RiskRewardRequest,
) -> Result<RiskRewardResponse, Error> {
let resp = self
.post("/v3/marketdata/options/riskreward", request)
.await?;
Ok(resp.json().await?)
}
}