Skip to main content

finance_query_core/models/
quote.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4#[serde(rename_all = "snake_case")]
5pub struct Quote {
6    pub symbol: String,
7    pub name: String,
8    pub price: String,
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub pre_market_price: Option<String>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub after_hours_price: Option<String>,
13    pub change: String,
14    pub percent_change: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub open: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub high: Option<String>,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub low: Option<String>,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub year_high: Option<String>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub year_low: Option<String>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub volume: Option<i64>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub avg_volume: Option<i64>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub market_cap: Option<String>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub beta: Option<String>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub pe: Option<String>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub eps: Option<String>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub dividend: Option<String>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub dividend_yield: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub ex_dividend: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub net_assets: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub nav: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub expense_ratio: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub category: Option<String>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub last_capital_gain: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub morningstar_rating: Option<String>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub morningstar_risk_rating: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub holdings_turnover: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub earnings_date: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub last_dividend: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub inception_date: Option<String>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub sector: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub industry: Option<String>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub about: Option<String>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub employees: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub five_days_return: Option<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub one_month_return: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub three_month_return: Option<String>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub six_month_return: Option<String>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub ytd_return: Option<String>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub year_return: Option<String>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub three_year_return: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub five_year_return: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub ten_year_return: Option<String>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub max_return: Option<String>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub logo: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub struct SimpleQuote {
100    pub symbol: String,
101    pub name: String,
102    pub price: String,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub pre_market_price: Option<String>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub after_hours_price: Option<String>,
107    pub change: String,
108    pub percent_change: String,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub logo: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct DetailedQuote {
115    pub symbol: String,
116    pub name: String,
117    pub price: String,
118    #[serde(skip_serializing_if = "Option::is_none", rename = "preMarketPrice")]
119    pub pre_market_price: Option<String>,
120    #[serde(skip_serializing_if = "Option::is_none", rename = "afterHoursPrice")]
121    pub after_hours_price: Option<String>,
122    pub change: String,
123    #[serde(rename = "percentChange")]
124    pub percent_change: String,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub open: Option<String>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub high: Option<String>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub low: Option<String>,
131    #[serde(skip_serializing_if = "Option::is_none", rename = "yearHigh")]
132    pub year_high: Option<String>,
133    #[serde(skip_serializing_if = "Option::is_none", rename = "yearLow")]
134    pub year_low: Option<String>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub volume: Option<i64>,
137    #[serde(skip_serializing_if = "Option::is_none", rename = "avgVolume")]
138    pub avg_volume: Option<i64>,
139    #[serde(skip_serializing_if = "Option::is_none", rename = "marketCap")]
140    pub market_cap: Option<String>,
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub beta: Option<String>,
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub pe: Option<String>,
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub eps: Option<String>,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub dividend: Option<String>,
149    #[serde(skip_serializing_if = "Option::is_none", rename = "yield")]
150    pub dividend_yield: Option<String>,
151    #[serde(skip_serializing_if = "Option::is_none", rename = "exDividend")]
152    pub ex_dividend: Option<String>,
153    #[serde(skip_serializing_if = "Option::is_none", rename = "netAssets")]
154    pub net_assets: Option<String>,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub nav: Option<String>,
157    #[serde(skip_serializing_if = "Option::is_none", rename = "expenseRatio")]
158    pub expense_ratio: Option<String>,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub category: Option<String>,
161    #[serde(skip_serializing_if = "Option::is_none", rename = "lastCapitalGain")]
162    pub last_capital_gain: Option<String>,
163    #[serde(skip_serializing_if = "Option::is_none", rename = "morningstarRating")]
164    pub morningstar_rating: Option<String>,
165    #[serde(
166        skip_serializing_if = "Option::is_none",
167        rename = "morningstarRiskRating"
168    )]
169    pub morningstar_risk_rating: Option<String>,
170    #[serde(skip_serializing_if = "Option::is_none", rename = "holdingsTurnover")]
171    pub holdings_turnover: Option<String>,
172    #[serde(skip_serializing_if = "Option::is_none", rename = "earningsDate")]
173    pub earnings_date: Option<String>,
174    #[serde(skip_serializing_if = "Option::is_none", rename = "lastDividend")]
175    pub last_dividend: Option<String>,
176    #[serde(skip_serializing_if = "Option::is_none", rename = "inceptionDate")]
177    pub inception_date: Option<String>,
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub sector: Option<String>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub industry: Option<String>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub about: Option<String>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub employees: Option<String>,
186    #[serde(skip_serializing_if = "Option::is_none", rename = "fiveDaysReturn")]
187    pub five_days_return: Option<String>,
188    #[serde(skip_serializing_if = "Option::is_none", rename = "oneMonthReturn")]
189    pub one_month_return: Option<String>,
190    #[serde(skip_serializing_if = "Option::is_none", rename = "threeMonthReturn")]
191    pub three_month_return: Option<String>,
192    #[serde(skip_serializing_if = "Option::is_none", rename = "sixMonthReturn")]
193    pub six_month_return: Option<String>,
194    #[serde(skip_serializing_if = "Option::is_none", rename = "ytdReturn")]
195    pub ytd_return: Option<String>,
196    #[serde(skip_serializing_if = "Option::is_none", rename = "yearReturn")]
197    pub year_return: Option<String>,
198    #[serde(skip_serializing_if = "Option::is_none", rename = "threeYearReturn")]
199    pub three_year_return: Option<String>,
200    #[serde(skip_serializing_if = "Option::is_none", rename = "fiveYearReturn")]
201    pub five_year_return: Option<String>,
202    #[serde(skip_serializing_if = "Option::is_none", rename = "tenYearReturn")]
203    pub ten_year_return: Option<String>,
204    #[serde(skip_serializing_if = "Option::is_none", rename = "maxReturn")]
205    pub max_return: Option<String>,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub logo: Option<String>,
208}
209
210impl From<Quote> for DetailedQuote {
211    fn from(quote: Quote) -> Self {
212        DetailedQuote {
213            symbol: quote.symbol,
214            name: quote.name,
215            price: quote.price,
216            pre_market_price: quote.pre_market_price,
217            after_hours_price: quote.after_hours_price,
218            change: quote.change,
219            percent_change: quote.percent_change,
220            open: quote.open,
221            high: quote.high,
222            low: quote.low,
223            year_high: quote.year_high,
224            year_low: quote.year_low,
225            volume: quote.volume,
226            avg_volume: quote.avg_volume,
227            market_cap: quote.market_cap,
228            beta: quote.beta,
229            pe: quote.pe,
230            eps: quote.eps,
231            dividend: quote.dividend,
232            dividend_yield: quote.dividend_yield,
233            ex_dividend: quote.ex_dividend,
234            net_assets: quote.net_assets,
235            nav: quote.nav,
236            expense_ratio: quote.expense_ratio,
237            category: quote.category,
238            last_capital_gain: quote.last_capital_gain,
239            morningstar_rating: quote.morningstar_rating,
240            morningstar_risk_rating: quote.morningstar_risk_rating,
241            holdings_turnover: quote.holdings_turnover,
242            earnings_date: quote.earnings_date,
243            last_dividend: quote.last_dividend,
244            inception_date: quote.inception_date,
245            sector: quote.sector,
246            industry: quote.industry,
247            about: quote.about,
248            employees: quote.employees,
249            five_days_return: quote.five_days_return,
250            one_month_return: quote.one_month_return,
251            three_month_return: quote.three_month_return,
252            six_month_return: quote.six_month_return,
253            ytd_return: quote.ytd_return,
254            year_return: quote.year_return,
255            three_year_return: quote.three_year_return,
256            five_year_return: quote.five_year_return,
257            ten_year_return: quote.ten_year_return,
258            max_return: quote.max_return,
259            logo: quote.logo,
260        }
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use proptest::prelude::*;
268
269    // Helper strategy for generating optional strings
270    fn optional_string() -> impl Strategy<Value = Option<String>> {
271        proptest::option::of("[a-zA-Z0-9 .-]{0,20}")
272    }
273
274    // Helper strategy for generating optional i64
275    fn optional_i64() -> impl Strategy<Value = Option<i64>> {
276        proptest::option::of(0i64..1_000_000_000i64)
277    }
278
279    // **Feature: crate-extraction, Property 1: Model Serialization Round-Trip**
280    // **Validates: Requirements 2.2**
281    proptest! {
282        #![proptest_config(ProptestConfig::with_cases(100))]
283
284        #[test]
285        fn simple_quote_roundtrip(
286            symbol in "[A-Z]{1,5}",
287            name in "[A-Za-z ]{1,50}",
288            price in "[0-9]{1,4}\\.[0-9]{2}",
289            pre_market_price in optional_string(),
290            after_hours_price in optional_string(),
291            change in "-?[0-9]{1,3}\\.[0-9]{2}",
292            percent_change in "-?[0-9]{1,3}\\.[0-9]{2}%",
293            logo in optional_string(),
294        ) {
295            let quote = SimpleQuote {
296                symbol: symbol.clone(),
297                name: name.clone(),
298                price: price.clone(),
299                pre_market_price: pre_market_price.clone(),
300                after_hours_price: after_hours_price.clone(),
301                change: change.clone(),
302                percent_change: percent_change.clone(),
303                logo: logo.clone(),
304            };
305
306            let json = serde_json::to_string(&quote).unwrap();
307            let parsed: SimpleQuote = serde_json::from_str(&json).unwrap();
308
309            prop_assert_eq!(quote.symbol, parsed.symbol);
310            prop_assert_eq!(quote.name, parsed.name);
311            prop_assert_eq!(quote.price, parsed.price);
312            prop_assert_eq!(quote.pre_market_price, parsed.pre_market_price);
313            prop_assert_eq!(quote.after_hours_price, parsed.after_hours_price);
314            prop_assert_eq!(quote.change, parsed.change);
315            prop_assert_eq!(quote.percent_change, parsed.percent_change);
316            prop_assert_eq!(quote.logo, parsed.logo);
317        }
318
319        #[test]
320        fn quote_roundtrip(
321            symbol in "[A-Z]{1,5}",
322            name in "[A-Za-z ]{1,50}",
323            price in "[0-9]{1,4}\\.[0-9]{2}",
324            pre_market_price in optional_string(),
325            after_hours_price in optional_string(),
326            change in "-?[0-9]{1,3}\\.[0-9]{2}",
327            percent_change in "-?[0-9]{1,3}\\.[0-9]{2}%",
328            open in optional_string(),
329            high in optional_string(),
330            low in optional_string(),
331            year_high in optional_string(),
332            year_low in optional_string(),
333            volume in optional_i64(),
334            avg_volume in optional_i64(),
335            market_cap in optional_string(),
336            logo in optional_string(),
337        ) {
338            let quote = Quote {
339                symbol: symbol.clone(),
340                name: name.clone(),
341                price: price.clone(),
342                pre_market_price: pre_market_price.clone(),
343                after_hours_price: after_hours_price.clone(),
344                change: change.clone(),
345                percent_change: percent_change.clone(),
346                open: open.clone(),
347                high: high.clone(),
348                low: low.clone(),
349                year_high: year_high.clone(),
350                year_low: year_low.clone(),
351                volume,
352                avg_volume,
353                market_cap: market_cap.clone(),
354                beta: None,
355                pe: None,
356                eps: None,
357                dividend: None,
358                dividend_yield: None,
359                ex_dividend: None,
360                net_assets: None,
361                nav: None,
362                expense_ratio: None,
363                category: None,
364                last_capital_gain: None,
365                morningstar_rating: None,
366                morningstar_risk_rating: None,
367                holdings_turnover: None,
368                earnings_date: None,
369                last_dividend: None,
370                inception_date: None,
371                sector: None,
372                industry: None,
373                about: None,
374                employees: None,
375                five_days_return: None,
376                one_month_return: None,
377                three_month_return: None,
378                six_month_return: None,
379                ytd_return: None,
380                year_return: None,
381                three_year_return: None,
382                five_year_return: None,
383                ten_year_return: None,
384                max_return: None,
385                logo: logo.clone(),
386            };
387
388            let json = serde_json::to_string(&quote).unwrap();
389            let parsed: Quote = serde_json::from_str(&json).unwrap();
390
391            prop_assert_eq!(quote.symbol, parsed.symbol);
392            prop_assert_eq!(quote.name, parsed.name);
393            prop_assert_eq!(quote.price, parsed.price);
394            prop_assert_eq!(quote.pre_market_price, parsed.pre_market_price);
395            prop_assert_eq!(quote.after_hours_price, parsed.after_hours_price);
396            prop_assert_eq!(quote.change, parsed.change);
397            prop_assert_eq!(quote.percent_change, parsed.percent_change);
398            prop_assert_eq!(quote.open, parsed.open);
399            prop_assert_eq!(quote.high, parsed.high);
400            prop_assert_eq!(quote.low, parsed.low);
401            prop_assert_eq!(quote.year_high, parsed.year_high);
402            prop_assert_eq!(quote.year_low, parsed.year_low);
403            prop_assert_eq!(quote.volume, parsed.volume);
404            prop_assert_eq!(quote.avg_volume, parsed.avg_volume);
405            prop_assert_eq!(quote.market_cap, parsed.market_cap);
406            prop_assert_eq!(quote.logo, parsed.logo);
407        }
408    }
409}