Skip to main content

tvdata_rs/forex/
mod.rs

1use crate::batch::BatchResult;
2use crate::client::TradingViewClient;
3use crate::error::Result;
4use crate::market_data::{
5    QuoteSnapshot, RowDecoder, SnapshotLoader, TechnicalSummary, decode_quote, decode_technical,
6    identity_columns, merge_columns, technical_columns,
7};
8use crate::scanner::fields::price;
9use crate::scanner::{Column, ScanQuery, SortOrder, Ticker};
10
11#[cfg(test)]
12mod tests;
13
14/// High-level FX market facade for quote snapshots, technicals, and movers.
15///
16/// # Examples
17///
18/// ```no_run
19/// use tvdata_rs::{Result, TradingViewClient};
20///
21/// #[tokio::main]
22/// async fn main() -> Result<()> {
23///     let client = TradingViewClient::builder().build()?;
24///     let quote = client.forex().quote("FX:EURUSD").await?;
25///
26///     println!("{:?}", quote.close);
27///     Ok(())
28/// }
29/// ```
30#[derive(Debug, Clone, Copy)]
31pub struct ForexClient<'a> {
32    client: &'a TradingViewClient,
33}
34
35impl<'a> ForexClient<'a> {
36    pub const fn new(client: &'a TradingViewClient) -> Self {
37        Self { client }
38    }
39
40    pub fn client(&self) -> &'a TradingViewClient {
41        self.client
42    }
43
44    /// Fetches a typed FX quote snapshot for a single symbol.
45    pub async fn quote(&self, symbol: impl Into<Ticker>) -> Result<QuoteSnapshot> {
46        let columns = forex_quote_columns();
47        let decoder = RowDecoder::new(&columns);
48        let row = self.loader().fetch_one(symbol, columns).await?;
49        Ok(decode_quote(&decoder, &row))
50    }
51
52    /// Fetches typed FX quote snapshots for multiple symbols while preserving the requested
53    /// order.
54    pub async fn quotes<I, T>(&self, symbols: I) -> Result<Vec<QuoteSnapshot>>
55    where
56        I: IntoIterator<Item = T>,
57        T: Into<Ticker>,
58    {
59        let columns = forex_quote_columns();
60        let decoder = RowDecoder::new(&columns);
61        let rows = self.loader().fetch_many(symbols, columns).await?;
62
63        Ok(rows
64            .iter()
65            .map(|row| decode_quote(&decoder, row))
66            .collect::<Vec<_>>())
67    }
68
69    pub async fn quotes_batch<I, T>(&self, symbols: I) -> Result<BatchResult<QuoteSnapshot>>
70    where
71        I: IntoIterator<Item = T>,
72        T: Into<Ticker>,
73    {
74        let columns = forex_quote_columns();
75        let decoder = RowDecoder::new(&columns);
76        let rows = self.loader().fetch_many_detailed(symbols, columns).await?;
77
78        Ok(BatchResult {
79            successes: rows
80                .successes
81                .into_iter()
82                .map(|(ticker, row)| (ticker, decode_quote(&decoder, &row)))
83                .collect(),
84            missing: rows.missing,
85            failures: rows.failures,
86        })
87    }
88
89    pub async fn technical_summary(&self, symbol: impl Into<Ticker>) -> Result<TechnicalSummary> {
90        let columns = technical_columns();
91        let decoder = RowDecoder::new(&columns);
92        let row = self.loader().fetch_one(symbol, columns).await?;
93        Ok(decode_technical(&decoder, &row))
94    }
95
96    pub async fn technical_summaries<I, T>(&self, symbols: I) -> Result<Vec<TechnicalSummary>>
97    where
98        I: IntoIterator<Item = T>,
99        T: Into<Ticker>,
100    {
101        let columns = technical_columns();
102        let decoder = RowDecoder::new(&columns);
103        let rows = self.loader().fetch_many(symbols, columns).await?;
104
105        Ok(rows
106            .iter()
107            .map(|row| decode_technical(&decoder, row))
108            .collect::<Vec<_>>())
109    }
110
111    pub async fn overview(&self, symbol: impl Into<Ticker>) -> Result<ForexOverview> {
112        let columns = overview_columns();
113        let decoder = RowDecoder::new(&columns);
114        let row = self.loader().fetch_one(symbol, columns).await?;
115        Ok(ForexOverview {
116            quote: decode_quote(&decoder, &row),
117            technicals: decode_technical(&decoder, &row),
118        })
119    }
120
121    pub async fn overviews<I, T>(&self, symbols: I) -> Result<Vec<ForexOverview>>
122    where
123        I: IntoIterator<Item = T>,
124        T: Into<Ticker>,
125    {
126        let columns = overview_columns();
127        let decoder = RowDecoder::new(&columns);
128        let rows = self.loader().fetch_many(symbols, columns).await?;
129
130        Ok(rows
131            .iter()
132            .map(|row| ForexOverview {
133                quote: decode_quote(&decoder, row),
134                technicals: decode_technical(&decoder, row),
135            })
136            .collect::<Vec<_>>())
137    }
138
139    pub async fn top_gainers(&self, limit: usize) -> Result<Vec<QuoteSnapshot>> {
140        self.loader()
141            .fetch_market_quotes_with_columns(
142                "forex",
143                limit,
144                price::CHANGE_PERCENT.sort(SortOrder::Desc),
145                forex_quote_columns(),
146                false,
147            )
148            .await
149    }
150
151    pub async fn top_losers(&self, limit: usize) -> Result<Vec<QuoteSnapshot>> {
152        self.loader()
153            .fetch_market_quotes_with_columns(
154                "forex",
155                limit,
156                price::CHANGE_PERCENT.sort(SortOrder::Asc),
157                forex_quote_columns(),
158                false,
159            )
160            .await
161    }
162
163    /// Fetches the most active FX instruments by TradingView volume.
164    ///
165    /// # Examples
166    ///
167    /// ```no_run
168    /// use tvdata_rs::{Result, TradingViewClient};
169    ///
170    /// #[tokio::main]
171    /// async fn main() -> Result<()> {
172    ///     let client = TradingViewClient::builder().build()?;
173    ///     let movers = client.forex().most_active(10).await?;
174    ///
175    ///     println!("movers: {}", movers.len());
176    ///     Ok(())
177    /// }
178    /// ```
179    pub async fn most_active(&self, limit: usize) -> Result<Vec<QuoteSnapshot>> {
180        self.loader()
181            .fetch_market_quotes_with_columns(
182                "forex",
183                limit,
184                price::VOLUME.sort(SortOrder::Desc),
185                forex_quote_columns(),
186                true,
187            )
188            .await
189    }
190
191    fn loader(&self) -> SnapshotLoader<'_> {
192        SnapshotLoader::new(
193            self.client,
194            ScanQuery::new().market("forex").symbol_types(["forex"]),
195        )
196    }
197}
198
199#[derive(Debug, Clone, PartialEq)]
200pub struct ForexOverview {
201    pub quote: QuoteSnapshot,
202    pub technicals: TechnicalSummary,
203}
204
205impl TradingViewClient {
206    /// Returns the high-level FX facade.
207    pub fn forex(&self) -> ForexClient<'_> {
208        ForexClient::new(self)
209    }
210}
211
212fn overview_columns() -> Vec<Column> {
213    merge_columns([forex_quote_columns(), technical_columns()])
214}
215
216fn forex_quote_columns() -> Vec<Column> {
217    merge_columns([
218        identity_columns(),
219        vec![
220            price::OPEN,
221            price::HIGH,
222            price::LOW,
223            price::CLOSE,
224            price::CHANGE_PERCENT,
225            price::CHANGE_ABS,
226            price::VOLUME,
227            price::RELATIVE_VOLUME,
228        ],
229    ])
230}