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#[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 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 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 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 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}