tradingview-rs 0.0.2

Tradingview datafeed api `tradingview-rs` project.
Documentation
use crate::{MarketSymbol, MarketType, websocket::SeriesInfo};
use bon::Builder;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, Deserialize, Serialize, Copy, PartialEq, Eq, Hash)]
pub enum ChartType {
    HeikinAshi,
    Renko,
    LineBreak,
    Kagi,
    PointAndFigure,
    Range,
}

impl std::fmt::Display for ChartType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let chart_type = match self {
            ChartType::HeikinAshi => "BarSetHeikenAshi@tv-basicstudies-60!",
            ChartType::Renko => "BarSetRenko@tv-prostudies-40!",
            ChartType::LineBreak => "BarSetPriceBreak@tv-prostudies-34!",
            ChartType::Kagi => "BarSetKagi@tv-prostudies-34!",
            ChartType::PointAndFigure => "BarSetPnF@tv-prostudies-34!",
            ChartType::Range => "BarSetRange@tv-basicstudies-72!",
        };
        write!(f, "{chart_type}")
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, Builder)]
pub struct ChartHistoricalData {
    pub symbol_info: SymbolInfo,
    pub series_info: SeriesInfo,
    pub data: Vec<DataPoint>,
}

impl ChartHistoricalData {
    pub fn new() -> Self {
        Self {
            symbol_info: SymbolInfo::default(),
            series_info: SeriesInfo::default(),
            data: Vec::new(),
        }
    }
}

impl Default for ChartHistoricalData {
    fn default() -> Self {
        Self::new()
    }
}

impl PriceIterable for ChartHistoricalData {
    type Item = DataPoint;

    fn to_vec(&self) -> impl Iterator<Item = &Self::Item> + '_ {
        self.data.to_vec()
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct ChartResponseData {
    #[serde(default)]
    pub node: Option<String>,
    #[serde(rename(deserialize = "s"))]
    pub series: Vec<DataPoint>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct StudyResponseData {
    #[serde(default)]
    pub node: Option<String>,
    #[serde(rename(deserialize = "st"))]
    pub studies: Vec<DataPoint>,
    #[serde(rename(deserialize = "ns"))]
    pub raw_graphics: GraphicDataResponse,
}

// TODO: Implement graphic parser for indexes response
#[derive(Debug, Clone, Deserialize)]
pub struct GraphicDataResponse {
    pub d: String,
    pub indexes: Value,
}

#[cfg_attr(not(feature = "protobuf"), derive(Debug, Default))]
#[cfg_attr(feature = "protobuf", derive(prost::Message))]
#[derive(Clone, Deserialize, Serialize, PartialEq)]
pub struct DataPoint {
    #[cfg_attr(feature = "protobuf", prost(int64, tag = "1"))]
    #[serde(rename(deserialize = "i"))]
    pub index: i64,
    #[cfg_attr(feature = "protobuf", prost(double, repeated, tag = "2"))]
    #[serde(rename(deserialize = "v"))]
    pub value: Vec<f64>,
}

pub trait PriceIterable {
    type Item: OHLCV;

    fn to_vec(&self) -> impl Iterator<Item = &Self::Item> + '_;

    fn closes(&self) -> impl Iterator<Item = f64> + '_ {
        self.to_vec().map(|dp| dp.close())
    }

    fn opens(&self) -> impl Iterator<Item = f64> + '_ {
        self.to_vec().map(|dp| dp.open())
    }

    fn highs(&self) -> impl Iterator<Item = f64> + '_ {
        self.to_vec().map(|dp| dp.high())
    }

    fn lows(&self) -> impl Iterator<Item = f64> + '_ {
        self.to_vec().map(|dp| dp.low())
    }

    fn volumes(&self) -> impl Iterator<Item = f64> + '_ {
        self.to_vec().map(|dp| dp.volume())
    }

    fn datetimes(&self) -> impl Iterator<Item = DateTime<Utc>> + '_ {
        self.to_vec().map(|dp| dp.datetime())
    }

    fn timestamps(&self) -> impl Iterator<Item = i64> + '_ {
        self.to_vec().map(|dp| dp.timestamp())
    }
}

impl PriceIterable for Vec<DataPoint> {
    type Item = DataPoint;

    fn to_vec(&self) -> impl Iterator<Item = &Self::Item> + '_ {
        self.iter()
    }
}

pub trait OHLCV {
    fn datetime(&self) -> DateTime<Utc>;
    fn timestamp(&self) -> i64;
    fn open(&self) -> f64;
    fn high(&self) -> f64;
    fn low(&self) -> f64;
    fn close(&self) -> f64;
    fn volume(&self) -> f64;
    fn is_ohlcv(&self) -> bool;
    fn validate(&self) -> bool {
        !(self.close() > self.high() || self.close() < self.low() || self.high() < self.low())
            && self.close() > 0.
            && self.open() > 0.
            && self.high() > 0.
            && self.low() > 0.
            && self.close().is_finite()
            && self.open().is_finite()
            && self.high().is_finite()
            && self.low().is_finite()
            && (self.volume().is_nan() || self.volume() >= 0.0)
    }

    fn tr(&self, prev_candle: &dyn OHLCV) -> f64 {
        self.tr_close(prev_candle.close())
    }

    fn tr_close(&self, prev_close: f64) -> f64 {
        self.high().max(prev_close) - self.low().min(prev_close)
    }

    fn clv(&self) -> f64 {
        if self.high() == self.low() {
            0.
        } else {
            let twice: f64 = 2.;
            (twice.mul_add(self.close(), -self.low()) - self.high()) / (self.high() - self.low())
        }
    }

    fn ohlc4(&self) -> f64 {
        (self.high() + self.low() + self.close() + self.open()) * 0.25
    }

    fn hl2(&self) -> f64 {
        (self.high() + self.low()) * 0.5
    }

    fn tp(&self) -> f64 {
        (self.high() + self.low() + self.close()) / 3.
    }

    fn volumed_price(&self) -> f64 {
        self.tp() * self.volume()
    }

    fn is_rising(&self) -> bool {
        self.close() > self.open()
    }

    fn is_falling(&self) -> bool {
        self.close() < self.open()
    }
}

impl OHLCV for DataPoint {
    fn datetime(&self) -> DateTime<Utc> {
        let timestamp = self.value[0] as i64;
        DateTime::<Utc>::from_timestamp(timestamp, 0).expect("Invalid timestamp")
    }

    fn timestamp(&self) -> i64 {
        self.value[0] as i64
    }

    fn open(&self) -> f64 {
        if !(self.is_ohlcv()) {
            return f64::NAN;
        }
        self.value[1]
    }

    fn high(&self) -> f64 {
        if !(self.is_ohlcv()) {
            return f64::NAN;
        }
        self.value[2]
    }

    fn low(&self) -> f64 {
        if !(self.is_ohlcv()) {
            return f64::NAN;
        }
        self.value[3]
    }

    fn close(&self) -> f64 {
        if !(self.is_ohlcv()) {
            return f64::NAN;
        }
        self.value[4]
    }

    fn volume(&self) -> f64 {
        if !self.is_ohlcv() {
            return f64::NAN;
        }
        self.value[5]
    }

    fn is_ohlcv(&self) -> bool {
        self.value.len() == 6
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct ChartDataChanges {
    pub changes: Vec<f64>,
    pub index: i64,
    pub index_diff: Vec<Value>,
    pub marks: Vec<Value>,
    pub zoffset: i64,
}

#[cfg_attr(not(feature = "protobuf"), derive(Debug, Default))]
#[cfg_attr(feature = "protobuf", derive(prost::Message))]
#[derive(Clone, PartialEq, Serialize, Hash)]
pub struct SeriesCompletedMessage {
    #[cfg_attr(feature = "protobuf", prost(string, tag = "1"))]
    #[serde(default)]
    pub id: String,
    #[cfg_attr(feature = "protobuf", prost(string, tag = "2"))]
    #[serde(default)]
    pub update_mode: String,
}

#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Default)]
#[serde(default)]
pub struct SymbolInfo {
    #[serde(rename(deserialize = "pro_name"))]
    pub id: String,

    #[serde(rename(deserialize = "original_name"))]
    pub original_name: String,

    pub name: String,
    pub exchange: String,
    pub description: String,

    #[serde(rename = "business_description")]
    pub business_description: String,

    #[serde(rename = "listed_exchange")]
    pub listed_exchange: String,

    #[serde(rename = "provider_id")]
    pub provider_id: String,

    #[serde(rename = "base_currency")]
    pub base_currency: String,

    #[serde(rename = "base_currency_id")]
    pub base_currency_id: String,

    #[serde(rename = "total_revenue")]
    pub total_revenue: f64,

    #[serde(rename = "price_earnings_ttm")]
    pub price_earnings_ttm: f64,

    #[serde(rename = "currency_id")]
    pub currency_id: String,

    #[serde(rename = "currency_code")]
    pub currency_code: String,

    pub session_holidays: String,

    pub subsessions: Vec<Subsession>,

    pub timezone: String,

    #[serde(rename(deserialize = "type"))]
    pub market_type: String,

    pub typespecs: Vec<String>,

    pub aliases: Vec<String>,

    pub total_shares_outstanding_calculated: f64,

    pub market_cap_basic: f64,

    pub earnings_release_date: i64,

    pub base_name: Vec<String>,

    pub sector: String,

    pub current_session: String,

    pub founded: u16,

    pub last_annual_eps: f64,

    pub fractional: bool,

    pub industry: String,
}

impl MarketSymbol for SymbolInfo {
    fn symbol(&self) -> &str {
        self.name.as_str()
    }

    fn exchange(&self) -> &str {
        self.exchange.as_str()
    }

    fn currency(&self) -> &str {
        &self.currency_id
    }

    fn id(&self) -> String {
        self.id.clone()
    }

    fn market_type(&self) -> MarketType {
        MarketType::from(self.market_type.as_str())
    }
}

#[cfg_attr(not(feature = "protobuf"), derive(Debug, Default))]
#[cfg_attr(feature = "protobuf", derive(prost::Message))]
#[derive(Clone, PartialEq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "camelCase", default)]
pub struct Subsession {
    #[cfg_attr(feature = "protobuf", prost(string, tag = "1"))]
    pub id: String,
    #[cfg_attr(feature = "protobuf", prost(string, tag = "2"))]
    pub description: String,
    #[cfg_attr(feature = "protobuf", prost(bool, tag = "3"))]
    pub private: bool,
    #[cfg_attr(feature = "protobuf", prost(string, tag = "4"))]
    pub session: String,
    #[cfg_attr(feature = "protobuf", prost(string, tag = "5"))]
    #[serde(rename(deserialize = "session-display"))]
    pub session_display: String,
}