1use exchange::{
2 Ticker, TickerStats,
3 adapter::{Exchange, MarketKind, Venue},
4 unit::{MinTicksize, price::Price},
5};
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8
9#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
10pub struct Settings {
11 pub favorited_tickers: Vec<Ticker>,
12 pub show_favorites: bool,
13 pub selected_sort_option: SortOptions,
14 pub selected_exchanges: Vec<Venue>,
15 pub selected_markets: Vec<MarketKind>,
16}
17
18impl Default for Settings {
19 fn default() -> Self {
20 Self {
21 favorited_tickers: vec![],
22 show_favorites: false,
23 selected_sort_option: SortOptions::VolumeDesc,
24 selected_exchanges: Venue::ALL.to_vec(),
25 selected_markets: MarketKind::ALL.into_iter().collect(),
26 }
27 }
28}
29
30#[derive(Default, Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
31pub enum SortOptions {
32 #[default]
33 VolumeAsc,
34 VolumeDesc,
35 ChangeAsc,
36 ChangeDesc,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum PriceChange {
41 Increased,
42 Decreased,
43}
44
45#[derive(Clone, Copy)]
46pub struct TickerRowData {
47 pub exchange: Exchange,
48 pub ticker: Ticker,
49 pub stats: TickerStats,
50 pub previous_stats: Option<TickerStats>,
51 pub is_favorited: bool,
52}
53
54#[derive(Clone)]
55pub struct TickerDisplayData {
56 pub display_ticker: String,
57 pub daily_change_pct: String,
58 pub volume_display: String,
59 pub mark_price_display: Option<String>,
60 pub price_unchanged_part: Option<String>,
61 pub price_changed_part: Option<String>,
62 pub price_change: Option<PriceChange>,
63 pub card_color_alpha: f32,
64}
65
66pub fn compare_ticker_rows_by_sort(
67 a: &TickerRowData,
68 b: &TickerRowData,
69 selected_sort_option: SortOptions,
70) -> Ordering {
71 match selected_sort_option {
72 SortOptions::VolumeDesc => b.stats.daily_volume.cmp(&a.stats.daily_volume),
73 SortOptions::VolumeAsc => a.stats.daily_volume.cmp(&b.stats.daily_volume),
74 SortOptions::ChangeDesc => b.stats.daily_price_chg.total_cmp(&a.stats.daily_price_chg),
75 SortOptions::ChangeAsc => a.stats.daily_price_chg.total_cmp(&b.stats.daily_price_chg),
76 }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub struct SearchRank {
88 pub bucket: u8,
89 pub pos: u16,
90 pub len: u16,
91}
92
93pub fn calc_search_rank(ticker: &Ticker, query: &str) -> Option<SearchRank> {
95 if query.is_empty() {
96 return Some(SearchRank {
97 bucket: 0,
98 pos: 0,
99 len: 0,
100 });
101 }
102
103 let (mut display_str, _) = ticker.display_symbol_and_type();
104 let (mut raw_str, _) = ticker.to_full_symbol_and_type();
105
106 display_str.make_ascii_uppercase();
107 raw_str.make_ascii_uppercase();
108
109 let suffix = market_suffix(ticker.market_type());
110 let is_perp = !suffix.is_empty();
111
112 let display_suffixed = format!("{display_str}{suffix}");
113 let raw_suffixed = format!("{raw_str}{suffix}");
114
115 let score_candidate = |cand: &str, allow_exact: bool| -> Option<SearchRank> {
118 let (bucket, pos) = if allow_exact && cand == query {
119 (0_u8, 0_usize) } else if cand.starts_with(query) {
121 (1_u8, 0_usize) } else if cand.ends_with(query) {
123 (2_u8, 0_usize) } else if let Some(p) = cand.find(query) {
125 (3_u8, p) } else {
127 return None;
128 };
129
130 Some(SearchRank {
131 bucket,
132 pos: (pos.min(u16::MAX as usize)) as u16,
133 len: (cand.len().min(u16::MAX as usize)) as u16,
134 })
135 };
136
137 let mut best: Option<SearchRank> = None;
138
139 for (cand, allow_exact) in [
142 (display_str.as_str(), !is_perp),
143 (display_suffixed.as_str(), true),
144 (raw_str.as_str(), !is_perp),
145 (raw_suffixed.as_str(), true),
146 ] {
147 let Some(rank) = score_candidate(cand, allow_exact) else {
148 continue;
149 };
150
151 best = Some(match best {
152 None => rank,
153 Some(cur) => {
154 if (rank.bucket, rank.pos, rank.len) < (cur.bucket, cur.pos, cur.len) {
156 rank
157 } else {
158 cur
159 }
160 }
161 });
162 }
163
164 best
165}
166
167pub fn market_suffix(market: MarketKind) -> &'static str {
168 match market {
169 MarketKind::Spot => "",
170 MarketKind::LinearPerps | MarketKind::InversePerps => "P",
171 }
172}
173
174pub fn compute_display_data(
175 ticker: &Ticker,
176 stats: &TickerStats,
177 previous_price: Option<Price>,
178 precision: Option<MinTicksize>,
179) -> TickerDisplayData {
180 let (display_ticker, _market) = ticker.display_symbol_and_type();
181
182 let current_price = stats.mark_price;
183 let current_price_display = price_to_display_string(current_price, precision);
184 let price_parts = previous_price
185 .and_then(|prev_price| split_price_changes(prev_price, current_price, precision))
186 .or_else(|| {
187 current_price_display
188 .clone()
189 .map(|price| (price, String::new(), None))
190 });
191
192 let (price_unchanged_part, price_changed_part, price_change) = price_parts
193 .map(|(unchanged, changed, change)| (Some(unchanged), Some(changed), change))
194 .unwrap_or((None, None, None));
195
196 TickerDisplayData {
197 display_ticker,
198 daily_change_pct: super::util::pct_change(stats.daily_price_chg),
199 volume_display: super::util::currency_abbr(stats.daily_volume.to_f32_lossy()),
200 mark_price_display: current_price_display,
201 price_unchanged_part,
202 price_changed_part,
203 price_change,
204 card_color_alpha: { (stats.daily_price_chg / 8.0).clamp(-1.0, 1.0) },
205 }
206}
207
208fn split_price_changes(
209 previous_price: Price,
210 current_price: Price,
211 precision: Option<MinTicksize>,
212) -> Option<(String, String, Option<PriceChange>)> {
213 let curr_str = price_to_display_string(current_price, precision)?;
214
215 if previous_price == current_price {
216 return Some((curr_str, String::new(), None));
217 }
218
219 let prev_str = price_to_display_string(previous_price, precision)?;
220
221 if prev_str == curr_str {
222 return Some((curr_str, String::new(), None));
223 }
224
225 let direction = Some(if current_price > previous_price {
226 PriceChange::Increased
227 } else {
228 PriceChange::Decreased
229 });
230
231 let split_index = prev_str
232 .bytes()
233 .zip(curr_str.bytes())
234 .position(|(prev, curr)| prev != curr)
235 .unwrap_or_else(|| prev_str.len().min(curr_str.len()));
236
237 let unchanged_part = curr_str[..split_index].to_string();
238 let changed_part = curr_str[split_index..].to_string();
239
240 Some((unchanged_part, changed_part, direction))
241}
242
243fn price_to_display_string(price: Price, precision: Option<MinTicksize>) -> Option<String> {
244 precision.map(|precision| price.to_string(precision))
245}