tvdata-rs 0.1.2

Async Rust client for TradingView screener queries, search, calendars, quote snapshots, and OHLCV history.
Documentation
use std::collections::BTreeMap;

use bon::Builder;
use serde::Deserialize;
use serde_json::Value;
use time::{Duration, OffsetDateTime, format_description::well_known::Rfc3339};

use crate::error::Result;

#[cfg(test)]
mod tests;

fn default_calendar_from() -> OffsetDateTime {
    OffsetDateTime::now_utc() - Duration::days(7)
}

fn default_calendar_to() -> OffsetDateTime {
    OffsetDateTime::now_utc() + Duration::days(7)
}

#[derive(Debug, Clone, PartialEq, Eq, Builder)]
pub struct EconomicCalendarRequest {
    #[builder(default = default_calendar_from())]
    pub from: OffsetDateTime,
    #[builder(default = default_calendar_to())]
    pub to: OffsetDateTime,
}

impl EconomicCalendarRequest {
    pub fn new(from: OffsetDateTime, to: OffsetDateTime) -> Self {
        Self::builder().from(from).to(to).build()
    }

    pub fn upcoming(days: i64) -> Self {
        let now = OffsetDateTime::now_utc();
        Self::builder()
            .from(now)
            .to(now + Duration::days(days.max(0)))
            .build()
    }

    pub fn trailing(days: i64) -> Self {
        let now = OffsetDateTime::now_utc();
        Self::builder()
            .from(now - Duration::days(days.max(0)))
            .to(now)
            .build()
    }

    pub fn to_query_pairs(&self) -> Result<Vec<(&'static str, String)>> {
        Ok(vec![
            ("from", self.from.format(&Rfc3339)?),
            ("to", self.to.format(&Rfc3339)?),
        ])
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct EconomicCalendarResponse {
    pub status: Option<String>,
    pub events: Vec<EconomicEvent>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct EconomicEvent {
    pub id: String,
    pub title: Option<String>,
    pub indicator: Option<String>,
    pub date: OffsetDateTime,
    pub country: Option<String>,
    pub currency: Option<String>,
    pub importance: Option<i32>,
    pub actual: Option<EconomicValue>,
    pub forecast: Option<EconomicValue>,
    pub previous: Option<EconomicValue>,
    pub period: Option<String>,
    pub scale: Option<String>,
    pub unit: Option<String>,
    pub source: Option<String>,
    pub comment: Option<String>,
    pub link: Option<String>,
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum EconomicValue {
    Number(f64),
    Text(String),
    Bool(bool),
}

#[derive(Debug, Clone, Deserialize)]
pub(crate) struct RawEconomicCalendarResponse {
    #[serde(default)]
    pub status: Option<String>,
    #[serde(default, rename = "result")]
    pub events: Vec<RawEconomicEvent>,
}

#[derive(Debug, Clone, Deserialize)]
pub(crate) struct RawEconomicEvent {
    pub id: String,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub indicator: Option<String>,
    #[serde(with = "time::serde::rfc3339")]
    pub date: OffsetDateTime,
    #[serde(default)]
    pub country: Option<String>,
    #[serde(default)]
    pub currency: Option<String>,
    #[serde(default)]
    pub importance: Option<i32>,
    #[serde(default)]
    pub actual: Option<EconomicValue>,
    #[serde(default)]
    pub forecast: Option<EconomicValue>,
    #[serde(default)]
    pub previous: Option<EconomicValue>,
    #[serde(default)]
    pub period: Option<String>,
    #[serde(default)]
    pub scale: Option<String>,
    #[serde(default)]
    pub unit: Option<String>,
    #[serde(default)]
    pub source: Option<String>,
    #[serde(default)]
    pub comment: Option<String>,
    #[serde(default)]
    pub link: Option<String>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

pub(crate) fn sanitize_calendar(
    raw_response: RawEconomicCalendarResponse,
) -> EconomicCalendarResponse {
    EconomicCalendarResponse {
        status: raw_response.status,
        events: raw_response
            .events
            .into_iter()
            .map(|event| EconomicEvent {
                id: event.id,
                title: event.title,
                indicator: event.indicator,
                date: event.date,
                country: event.country,
                currency: event.currency,
                importance: event.importance,
                actual: event.actual,
                forecast: event.forecast,
                previous: event.previous,
                period: event.period,
                scale: event.scale,
                unit: event.unit,
                source: event.source,
                comment: event.comment,
                link: event.link,
                extra: event.extra,
            })
            .collect(),
    }
}