1use std::borrow::Cow;
2use std::fmt;
3
4use serde::{Serialize, Serializer};
5
6use crate::scanner::filter::{
7 FilterCondition, FilterOperator, IntoFilterValue, SortOrder, SortSpec,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
11pub struct Column(Cow<'static, str>);
12
13impl Column {
14 pub const fn from_static(name: &'static str) -> Self {
15 Self(Cow::Borrowed(name))
16 }
17
18 pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
19 Self(name.into())
20 }
21
22 pub fn as_str(&self) -> &str {
23 self.0.as_ref()
24 }
25
26 pub fn with_interval(&self, interval: &str) -> Self {
27 Self::new(format!("{}|{interval}", self.as_str()))
28 }
29
30 pub fn with_history(&self, periods: u16) -> Self {
31 Self::new(format!("{}[{periods}]", self.as_str()))
32 }
33
34 pub fn recommendation(&self) -> Self {
35 Self::new(format!("Rec.{}", self.as_str()))
36 }
37
38 pub fn gt(self, value: impl IntoFilterValue) -> FilterCondition {
39 FilterCondition::new(self, FilterOperator::Greater, value.into_filter_value())
40 }
41
42 pub fn ge(self, value: impl IntoFilterValue) -> FilterCondition {
43 FilterCondition::new(self, FilterOperator::EGreater, value.into_filter_value())
44 }
45
46 pub fn lt(self, value: impl IntoFilterValue) -> FilterCondition {
47 FilterCondition::new(self, FilterOperator::Less, value.into_filter_value())
48 }
49
50 pub fn le(self, value: impl IntoFilterValue) -> FilterCondition {
51 FilterCondition::new(self, FilterOperator::ELess, value.into_filter_value())
52 }
53
54 pub fn eq(self, value: impl IntoFilterValue) -> FilterCondition {
55 FilterCondition::new(self, FilterOperator::Equal, value.into_filter_value())
56 }
57
58 pub fn ne(self, value: impl IntoFilterValue) -> FilterCondition {
59 FilterCondition::new(self, FilterOperator::NotEqual, value.into_filter_value())
60 }
61
62 pub fn between(
63 self,
64 lower: impl IntoFilterValue,
65 upper: impl IntoFilterValue,
66 ) -> FilterCondition {
67 FilterCondition::new(
68 self,
69 FilterOperator::InRange,
70 vec![lower.into_filter_value(), upper.into_filter_value()].into_filter_value(),
71 )
72 }
73
74 pub fn not_between(
75 self,
76 lower: impl IntoFilterValue,
77 upper: impl IntoFilterValue,
78 ) -> FilterCondition {
79 FilterCondition::new(
80 self,
81 FilterOperator::NotInRange,
82 vec![lower.into_filter_value(), upper.into_filter_value()].into_filter_value(),
83 )
84 }
85
86 pub fn isin<I, V>(self, values: I) -> FilterCondition
87 where
88 I: IntoIterator<Item = V>,
89 V: IntoFilterValue,
90 {
91 FilterCondition::new(
92 self,
93 FilterOperator::InRange,
94 values
95 .into_iter()
96 .map(IntoFilterValue::into_filter_value)
97 .collect::<Vec<_>>()
98 .into_filter_value(),
99 )
100 }
101
102 pub fn not_in<I, V>(self, values: I) -> FilterCondition
103 where
104 I: IntoIterator<Item = V>,
105 V: IntoFilterValue,
106 {
107 FilterCondition::new(
108 self,
109 FilterOperator::NotInRange,
110 values
111 .into_iter()
112 .map(IntoFilterValue::into_filter_value)
113 .collect::<Vec<_>>()
114 .into_filter_value(),
115 )
116 }
117
118 pub fn crosses(self, value: impl IntoFilterValue) -> FilterCondition {
119 FilterCondition::new(self, FilterOperator::Crosses, value.into_filter_value())
120 }
121
122 pub fn crosses_above(self, value: impl IntoFilterValue) -> FilterCondition {
123 FilterCondition::new(
124 self,
125 FilterOperator::CrossesAbove,
126 value.into_filter_value(),
127 )
128 }
129
130 pub fn crosses_below(self, value: impl IntoFilterValue) -> FilterCondition {
131 FilterCondition::new(
132 self,
133 FilterOperator::CrossesBelow,
134 value.into_filter_value(),
135 )
136 }
137
138 pub fn matches(self, value: impl IntoFilterValue) -> FilterCondition {
139 FilterCondition::new(self, FilterOperator::Match, value.into_filter_value())
140 }
141
142 pub fn empty(self) -> FilterCondition {
143 FilterCondition::new(self, FilterOperator::Empty, serde_json::Value::Null)
144 }
145
146 pub fn not_empty(self) -> FilterCondition {
147 FilterCondition::new(self, FilterOperator::NotEmpty, serde_json::Value::Null)
148 }
149
150 pub fn above_pct(
151 self,
152 base: impl IntoFilterValue,
153 pct: impl IntoFilterValue,
154 ) -> FilterCondition {
155 FilterCondition::new(
156 self,
157 FilterOperator::AbovePercent,
158 vec![base.into_filter_value(), pct.into_filter_value()].into_filter_value(),
159 )
160 }
161
162 pub fn below_pct(
163 self,
164 base: impl IntoFilterValue,
165 pct: impl IntoFilterValue,
166 ) -> FilterCondition {
167 FilterCondition::new(
168 self,
169 FilterOperator::BelowPercent,
170 vec![base.into_filter_value(), pct.into_filter_value()].into_filter_value(),
171 )
172 }
173
174 pub fn sort(self, order: SortOrder) -> SortSpec {
175 SortSpec::new(self, order)
176 }
177}
178
179impl fmt::Display for Column {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 f.write_str(self.as_str())
182 }
183}
184
185impl Serialize for Column {
186 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
187 where
188 S: Serializer,
189 {
190 serializer.serialize_str(self.as_str())
191 }
192}
193
194impl From<&'static str> for Column {
195 fn from(value: &'static str) -> Self {
196 Self::from_static(value)
197 }
198}
199
200impl From<String> for Column {
201 fn from(value: String) -> Self {
202 Self::new(value)
203 }
204}
205
206impl From<&String> for Column {
207 fn from(value: &String) -> Self {
208 Self::new(value.clone())
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Hash)]
213pub struct Market(Cow<'static, str>);
214
215impl Market {
216 pub const fn from_static(name: &'static str) -> Self {
217 Self(Cow::Borrowed(name))
218 }
219
220 pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
221 Self(name.into())
222 }
223
224 pub fn as_str(&self) -> &str {
225 self.0.as_ref()
226 }
227}
228
229impl Serialize for Market {
230 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231 where
232 S: Serializer,
233 {
234 serializer.serialize_str(self.as_str())
235 }
236}
237
238impl From<&'static str> for Market {
239 fn from(value: &'static str) -> Self {
240 Self::from_static(value)
241 }
242}
243
244impl From<String> for Market {
245 fn from(value: String) -> Self {
246 Self::new(value)
247 }
248}
249
250pub trait SymbolNormalizer {
251 fn normalize(&self, instrument: &InstrumentRef) -> InstrumentRef;
252}
253
254#[derive(Debug, Clone, Copy, Default)]
255pub struct HeuristicSymbolNormalizer;
256
257impl HeuristicSymbolNormalizer {
258 fn normalize_symbol_for_exchange(exchange: &str, symbol: &str) -> String {
259 let uppercase = symbol.to_ascii_uppercase();
260
261 match exchange {
262 "FX" | "FX_IDC" | "FOREX" | "OANDA" | "FOREXCOM" => uppercase
263 .chars()
264 .filter(|ch| ch.is_ascii_alphanumeric())
265 .collect(),
266 "NYSE" | "NASDAQ" | "AMEX" | "ARCA" | "BATS" | "TSX" => uppercase.replace('-', "."),
267 _ => uppercase,
268 }
269 }
270}
271
272impl SymbolNormalizer for HeuristicSymbolNormalizer {
273 fn normalize(&self, instrument: &InstrumentRef) -> InstrumentRef {
274 InstrumentRef {
275 exchange: instrument.exchange.trim().to_ascii_uppercase(),
276 symbol: Self::normalize_symbol_for_exchange(&instrument.exchange, &instrument.symbol),
277 }
278 }
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
282pub struct InstrumentRef {
283 pub exchange: String,
284 pub symbol: String,
285}
286
287impl InstrumentRef {
288 pub fn new(exchange: impl Into<String>, symbol: impl Into<String>) -> Self {
289 Self {
290 exchange: exchange.into().trim().to_ascii_uppercase(),
291 symbol: symbol.into().trim().to_owned(),
292 }
293 }
294
295 pub fn from_exchange_symbol(exchange: impl Into<String>, symbol: impl Into<String>) -> Self {
300 Self::from_exchange_symbol_normalized(exchange, symbol)
301 }
302
303 pub fn from_exchange_symbol_normalized(
304 exchange: impl Into<String>,
305 symbol: impl Into<String>,
306 ) -> Self {
307 Self::new(exchange, symbol).normalized_with(&HeuristicSymbolNormalizer)
308 }
309
310 pub fn from_internal_us_equity(exchange: impl Into<String>, symbol: impl Into<String>) -> Self {
311 let exchange = exchange.into().trim().to_ascii_uppercase();
312 let symbol = symbol.into().trim().to_ascii_uppercase().replace('-', ".");
313 Self { exchange, symbol }
314 }
315
316 pub fn to_ticker(&self) -> Ticker {
317 Ticker::from_parts(&self.exchange, &self.symbol)
318 }
319
320 pub fn normalized_with<N>(&self, normalizer: &N) -> Self
321 where
322 N: SymbolNormalizer + ?Sized,
323 {
324 normalizer.normalize(self)
325 }
326
327 pub fn to_ticker_with<N>(&self, normalizer: &N) -> Ticker
328 where
329 N: SymbolNormalizer + ?Sized,
330 {
331 self.normalized_with(normalizer).to_ticker()
332 }
333
334 pub fn to_normalized_ticker(&self) -> Ticker {
335 self.to_ticker_with(&HeuristicSymbolNormalizer)
336 }
337}
338
339#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
340pub struct Ticker(Cow<'static, str>);
341
342impl Ticker {
343 pub fn from_parts(exchange: &str, symbol: &str) -> Self {
344 Self(Cow::Owned(format!("{exchange}:{symbol}")))
345 }
346
347 pub fn from_exchange_symbol(exchange: &str, symbol: &str) -> Self {
349 Self::from_exchange_symbol_normalized(exchange, symbol)
350 }
351
352 pub fn from_exchange_symbol_normalized(exchange: &str, symbol: &str) -> Self {
353 InstrumentRef::from_exchange_symbol_normalized(exchange, symbol).to_ticker()
354 }
355
356 pub fn from_exchange_symbol_with<N>(exchange: &str, symbol: &str, normalizer: &N) -> Self
357 where
358 N: SymbolNormalizer + ?Sized,
359 {
360 InstrumentRef::new(exchange, symbol).to_ticker_with(normalizer)
361 }
362
363 pub const fn from_static(raw: &'static str) -> Self {
364 Self(Cow::Borrowed(raw))
365 }
366
367 pub fn new(raw: impl Into<Cow<'static, str>>) -> Self {
368 Self(raw.into())
369 }
370
371 pub fn as_str(&self) -> &str {
372 self.0.as_ref()
373 }
374
375 pub fn split(&self) -> Option<(&str, &str)> {
376 self.as_str().split_once(':')
377 }
378
379 pub fn exchange(&self) -> Option<&str> {
380 self.split().map(|(exchange, _)| exchange)
381 }
382
383 pub fn symbol(&self) -> Option<&str> {
384 self.split().map(|(_, symbol)| symbol)
385 }
386}
387
388impl fmt::Display for Ticker {
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.write_str(self.as_str())
391 }
392}
393
394impl Serialize for Ticker {
395 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
396 where
397 S: Serializer,
398 {
399 serializer.serialize_str(self.as_str())
400 }
401}
402
403impl From<&'static str> for Ticker {
404 fn from(value: &'static str) -> Self {
405 Self::from_static(value)
406 }
407}
408
409impl From<String> for Ticker {
410 fn from(value: String) -> Self {
411 Self::new(value)
412 }
413}
414
415impl From<InstrumentRef> for Ticker {
416 fn from(value: InstrumentRef) -> Self {
417 value.to_ticker()
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[derive(Debug, Clone, Copy)]
426 struct CustomNormalizer;
427
428 impl SymbolNormalizer for CustomNormalizer {
429 fn normalize(&self, instrument: &InstrumentRef) -> InstrumentRef {
430 InstrumentRef::new(&instrument.exchange, instrument.symbol.replace('/', "-"))
431 }
432 }
433
434 #[test]
435 fn raw_ticker_construction_preserves_symbol_shape() {
436 let instrument = InstrumentRef::new("NYSE", "BRK-B");
437 assert_eq!(instrument.to_ticker().as_str(), "NYSE:BRK-B");
438 }
439
440 #[test]
441 fn heuristic_normalizer_handles_common_us_equity_symbols() {
442 let ticker = InstrumentRef::new("NYSE", "BRK-B").to_normalized_ticker();
443 assert_eq!(ticker.as_str(), "NYSE:BRK.B");
444 }
445
446 #[test]
447 fn heuristic_normalizer_handles_common_forex_pairs() {
448 let instrument = InstrumentRef::new("FX", "eur/usd");
449 assert_eq!(instrument.to_normalized_ticker().as_str(), "FX:EURUSD");
450 }
451
452 #[test]
453 fn custom_normalizer_can_override_symbol_rules() {
454 let ticker = Ticker::from_exchange_symbol_with("FX", "eur/usd", &CustomNormalizer);
455 assert_eq!(ticker.as_str(), "FX:eur-usd");
456 }
457}