Skip to main content

paft_domain/
exchange.rs

1//! Exchange enumeration with major exchanges and extensible fallback.
2//!
3//! This module provides type-safe handling of exchange identifiers while gracefully
4//! handling unknown or provider-specific exchanges through the `Other` variant.
5
6use crate::error::DomainError;
7// no module-level serde imports needed here
8use std::borrow::Cow;
9use std::str::FromStr;
10
11paft_core::other_string_code_type!(
12    /// Provider-specific exchange code that is not modeled by [`Exchange`].
13    pub struct OtherExchange for Exchange;
14    type Error = DomainError;
15    parse(input) => Exchange::try_from_str(input);
16    invalid(input) => DomainError::InvalidExchangeValue {
17        value: input.to_string(),
18    };
19);
20
21/// Exchange enumeration with major exchanges and extensible fallback.
22///
23/// This enum provides type-safe handling of exchange identifiers while gracefully
24/// handling unknown or provider-specific exchanges through the `Other` variant.
25///
26/// Canonical/serde rules:
27/// - Emission uses a single canonical form per variant (UPPERCASE ASCII, no spaces)
28/// - Parser accepts a superset of tokens (aliases, case-insensitive)
29/// - `Other(s)` serializes to its canonical `code()` string (no escape prefix)
30/// - `Display` output matches the canonical code for known variants and the raw `s` for `Other(s)`
31/// - Serde round-trips preserve identity for canonical variants; unknown tokens normalize to `Other(UPPERCASE)`
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33#[non_exhaustive]
34pub enum Exchange {
35    /// NASDAQ Stock Market
36    NASDAQ,
37    /// New York Stock Exchange
38    NYSE,
39    /// American Stock Exchange
40    AMEX,
41    /// BATS Global Markets
42    BATS,
43    /// Over-the-Counter Markets
44    OTC,
45    /// London Stock Exchange
46    LSE,
47    /// Tokyo Stock Exchange
48    TSE,
49    /// Hong Kong Stock Exchange
50    HKEX,
51    /// Shanghai Stock Exchange
52    SSE,
53    /// Shenzhen Stock Exchange
54    SZSE,
55    /// Toronto Stock Exchange
56    TSX,
57    /// Australian Securities Exchange
58    ASX,
59    /// Euronext
60    Euronext,
61    /// Deutsche Börse (XETRA)
62    XETRA,
63    /// Swiss Exchange
64    SIX,
65    /// Borsa Italiana
66    BIT,
67    /// Bolsa de Madrid
68    BME,
69    /// Euronext Amsterdam
70    AEX,
71    /// Euronext Brussels
72    BRU,
73    /// Euronext Lisbon
74    LIS,
75    /// Euronext Paris
76    EPA,
77    /// Oslo Børs
78    OSL,
79    /// Stockholm Stock Exchange
80    STO,
81    /// Copenhagen Stock Exchange
82    CPH,
83    /// Warsaw Stock Exchange
84    WSE,
85    /// Prague Stock Exchange
86    #[allow(non_camel_case_types)]
87    PSE_CZ,
88    /// Budapest Stock Exchange
89    #[allow(non_camel_case_types)]
90    BSE_HU,
91    /// Moscow Exchange
92    MOEX,
93    /// Istanbul Stock Exchange
94    BIST,
95    /// Johannesburg Stock Exchange
96    JSE,
97    /// Tel Aviv Stock Exchange
98    TASE,
99    /// Bombay Stock Exchange
100    BSE,
101    /// National Stock Exchange of India
102    NSE,
103    /// Korea Exchange
104    KRX,
105    /// Singapore Exchange
106    SGX,
107    /// Thailand Stock Exchange
108    SET,
109    /// Bursa Malaysia
110    KLSE,
111    /// Philippine Stock Exchange
112    PSE,
113    /// Indonesia Stock Exchange
114    IDX,
115    /// Ho Chi Minh Stock Exchange
116    HOSE,
117    /// Unknown or provider-specific exchange
118    Other(OtherExchange),
119}
120
121impl Exchange {
122    /// Attempts to parse an exchange identifier.
123    ///
124    /// # Errors
125    ///
126    /// Returns an error if `input` is empty or contains only whitespace.
127    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
128    pub fn try_from_str(input: &str) -> Result<Self, DomainError> {
129        let trimmed = input.trim();
130        if trimmed.is_empty() {
131            return Err(DomainError::InvalidExchangeValue {
132                value: input.to_string(),
133            });
134        }
135
136        Self::from_str(trimmed).map_err(|_| DomainError::InvalidExchangeValue {
137            value: input.to_string(),
138        })
139    }
140
141    /// Builds an unknown exchange value, rejecting tokens modeled by [`Exchange`].
142    ///
143    /// # Errors
144    ///
145    /// Returns an error if `input` is empty, cannot be canonicalized, or parses
146    /// to a modeled [`Exchange`] variant.
147    pub fn other(input: &str) -> Result<Self, DomainError> {
148        OtherExchange::new(input).map(Self::Other)
149    }
150
151    /// Returns true if this is a major US exchange
152    #[must_use]
153    pub const fn is_us_exchange(&self) -> bool {
154        matches!(
155            self,
156            Self::NASDAQ | Self::NYSE | Self::AMEX | Self::BATS | Self::OTC
157        )
158    }
159
160    /// Returns true if this is a European exchange
161    #[must_use]
162    pub const fn is_european_exchange(&self) -> bool {
163        matches!(
164            self,
165            Self::LSE
166                | Self::Euronext
167                | Self::XETRA
168                | Self::SIX
169                | Self::BIT
170                | Self::BME
171                | Self::AEX
172                | Self::BRU
173                | Self::LIS
174                | Self::EPA
175                | Self::OSL
176                | Self::STO
177                | Self::CPH
178                | Self::WSE
179                | Self::PSE_CZ
180                | Self::BSE_HU
181        )
182    }
183
184    /// Returns the human-readable name for this exchange.
185    #[must_use]
186    pub fn full_name(&self) -> Cow<'static, str> {
187        match self {
188            Self::NASDAQ => Cow::Borrowed("Nasdaq"),
189            Self::NYSE => Cow::Borrowed("NYSE"),
190            Self::AMEX => Cow::Borrowed("AMEX"),
191            Self::BATS => Cow::Borrowed("BATS"),
192            Self::OTC => Cow::Borrowed("OTC"),
193            Self::LSE => Cow::Borrowed("London Stock Exchange"),
194            Self::TSE => Cow::Borrowed("Tokyo Stock Exchange"),
195            Self::HKEX => Cow::Borrowed("Hong Kong Stock Exchange"),
196            Self::SSE => Cow::Borrowed("Shanghai Stock Exchange"),
197            Self::SZSE => Cow::Borrowed("Shenzhen Stock Exchange"),
198            Self::TSX => Cow::Borrowed("Toronto Stock Exchange"),
199            Self::ASX => Cow::Borrowed("Australian Securities Exchange"),
200            Self::Euronext => Cow::Borrowed("Euronext"),
201            Self::XETRA => Cow::Borrowed("Xetra"),
202            Self::SIX => Cow::Borrowed("Swiss Exchange"),
203            Self::BIT => Cow::Borrowed("Borsa Italiana"),
204            Self::BME => Cow::Borrowed("Bolsa de Madrid"),
205            Self::AEX => Cow::Borrowed("Euronext Amsterdam"),
206            Self::BRU => Cow::Borrowed("Euronext Brussels"),
207            Self::LIS => Cow::Borrowed("Euronext Lisbon"),
208            Self::EPA => Cow::Borrowed("Euronext Paris"),
209            Self::OSL => Cow::Borrowed("Oslo Børs"),
210            Self::STO => Cow::Borrowed("Stockholm Stock Exchange"),
211            Self::CPH => Cow::Borrowed("Copenhagen Stock Exchange"),
212            Self::WSE => Cow::Borrowed("Warsaw Stock Exchange"),
213            Self::PSE_CZ => Cow::Borrowed("Prague Stock Exchange"),
214            Self::BSE_HU => Cow::Borrowed("Budapest Stock Exchange"),
215            Self::MOEX => Cow::Borrowed("Moscow Exchange"),
216            Self::BIST => Cow::Borrowed("Istanbul Stock Exchange"),
217            Self::JSE => Cow::Borrowed("Johannesburg Stock Exchange"),
218            Self::TASE => Cow::Borrowed("Tel Aviv Stock Exchange"),
219            Self::BSE => Cow::Borrowed("Bombay Stock Exchange"),
220            Self::NSE => Cow::Borrowed("National Stock Exchange of India"),
221            Self::KRX => Cow::Borrowed("Korea Exchange"),
222            Self::SGX => Cow::Borrowed("Singapore Exchange"),
223            Self::SET => Cow::Borrowed("Stock Exchange of Thailand"),
224            Self::KLSE => Cow::Borrowed("Bursa Malaysia"),
225            Self::PSE => Cow::Borrowed("Philippine Stock Exchange"),
226            Self::IDX => Cow::Borrowed("Indonesia Stock Exchange"),
227            Self::HOSE => Cow::Borrowed("Ho Chi Minh Stock Exchange"),
228            Self::Other(code) => Cow::Owned(code.as_ref().to_string()),
229        }
230    }
231}
232
233// Implement code() and string impls via macro (open enum)
234crate::string_enum_with_code!(
235    Exchange, Other(OtherExchange), "Exchange",
236    type Error = DomainError;
237    invalid(input) => DomainError::InvalidExchangeValue {
238        value: input.to_string(),
239    };
240    {
241        "NASDAQ" => Exchange::NASDAQ,
242        "NYSE" => Exchange::NYSE,
243        "AMEX" => Exchange::AMEX,
244        "BATS" => Exchange::BATS,
245        "OTC" => Exchange::OTC,
246        "LSE" => Exchange::LSE,
247        "TSE" => Exchange::TSE,
248        "HKEX" => Exchange::HKEX,
249        "SSE" => Exchange::SSE,
250        "SZSE" => Exchange::SZSE,
251        "TSX" => Exchange::TSX,
252        "ASX" => Exchange::ASX,
253        "EURONEXT" => Exchange::Euronext,
254        "XETRA" => Exchange::XETRA,
255        "SIX" => Exchange::SIX,
256        "BIT" => Exchange::BIT,
257        "BME" => Exchange::BME,
258        "AEX" => Exchange::AEX,
259        "BRU" => Exchange::BRU,
260        "LIS" => Exchange::LIS,
261        "EPA" => Exchange::EPA,
262        "OSL" => Exchange::OSL,
263        "STO" => Exchange::STO,
264        "CPH" => Exchange::CPH,
265        "WSE" => Exchange::WSE,
266        "PSE_CZ" => Exchange::PSE_CZ,
267        "BSE_HU" => Exchange::BSE_HU,
268        "MOEX" => Exchange::MOEX,
269        "BIST" => Exchange::BIST,
270        "JSE" => Exchange::JSE,
271        "TASE" => Exchange::TASE,
272        "BSE" => Exchange::BSE,
273        "NSE" => Exchange::NSE,
274        "KRX" => Exchange::KRX,
275        "SGX" => Exchange::SGX,
276        "SET" => Exchange::SET,
277        "KLSE" => Exchange::KLSE,
278        "PSE" => Exchange::PSE,
279        "IDX" => Exchange::IDX,
280        "HOSE" => Exchange::HOSE
281    },
282    {
283        // Provider aliases
284        "EURONEXT_PARIS" => Exchange::EPA,
285        "BOMBAY" => Exchange::BSE,
286        "BSE_INDIA" => Exchange::BSE
287    }
288);
289
290crate::impl_display_via_code!(Exchange);