exc_symbol/
lib.rs

1use positions::{prelude::Str, Asset, Instrument, ParseSymbolError, Symbol};
2use rust_decimal::Decimal;
3use std::{borrow::Borrow, fmt, str::FromStr};
4use thiserror::Error;
5use time::{formatting::Formattable, macros::format_description, parsing::Parsable, Date};
6
7/// The exc format symbol.
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
9pub struct ExcSymbol(Symbol);
10
11impl AsRef<Symbol> for ExcSymbol {
12    fn as_ref(&self) -> &Symbol {
13        &self.0
14    }
15}
16
17/// Options Type.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum OptionsType {
20    /// Put.
21    Put(Str),
22    /// Call.
23    Call(Str),
24}
25
26/// Symbol Type.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum SymbolType {
29    /// Spot.
30    Spot,
31    /// Margin.
32    Margin,
33    /// Futures.
34    Futures(Str),
35    /// Perpetual.
36    Perpetual,
37    /// Options.
38    Options(Str, OptionsType),
39}
40
41impl ExcSymbol {
42    /// Margin tag.
43    pub const MARGIN: &'static str = "";
44    /// Futures tag.
45    pub const FUTURES: &'static str = "F";
46    /// Perpetual tag.
47    pub const PERPETUAL: &'static str = "P";
48    /// Options tag.
49    pub const OPTIONS: &'static str = "O";
50    /// Put options tag.
51    pub const PUT: &'static str = "P";
52    /// Call options tag.
53    pub const CALL: &'static str = "C";
54    /// Seperate tag.
55    pub const SEP: char = '-';
56
57    /// Get date format.
58    fn date_format() -> impl Parsable {
59        format_description!("[year][month][day]")
60    }
61
62    /// Get date format for formatting.
63    fn formatting_date_format() -> impl Formattable {
64        format_description!("[year repr:last_two][month][day]")
65    }
66
67    /// Create a symbol for spot.
68    pub fn spot(base: &Asset, quote: &Asset) -> Self {
69        Self(Symbol::spot(base, quote))
70    }
71
72    /// Create a symbol for margin.
73    pub fn margin(base: &Asset, quote: &Asset) -> Self {
74        Self(Symbol::derivative("", &format!("{base}-{quote}")).expect("must be valid"))
75    }
76
77    /// Create a symbol for perpetual.
78    pub fn perpetual(base: &Asset, quote: &Asset) -> Self {
79        Self(
80            Symbol::derivative(Self::PERPETUAL, &format!("{base}-{quote}")).expect("must be valid"),
81        )
82    }
83
84    /// Create a symbol for futures.
85    /// Return `None` if `date` cannot be parsed by the date format.
86    pub fn futures(base: &Asset, quote: &Asset, date: Date) -> Option<Self> {
87        let format = Self::formatting_date_format();
88        let date = date.format(&format).ok()?;
89        Some(Self(
90            Symbol::derivative(
91                &format!("{}{date}", Self::FUTURES),
92                &format!("{base}-{quote}"),
93            )
94            .expect("must be valid"),
95        ))
96    }
97
98    #[inline]
99    fn parse_date(s: &str) -> Option<Date> {
100        let format = Self::date_format();
101        Date::parse(&format!("20{s}"), &format).ok()
102    }
103
104    /// Create a symbol for futures with the given date in string.
105    /// Return `None` if `date` cannot be parsed by the date format.
106    pub fn futures_with_str(base: &Asset, quote: &Asset, date: &str) -> Option<Self> {
107        let date = Self::parse_date(date)?;
108        Self::futures(base, quote, date)
109    }
110
111    /// Create a symbol for put options.
112    /// Return `None` if `date` cannot be parsed by the date format.
113    pub fn put(base: &Asset, quote: &Asset, date: Date, price: Decimal) -> Option<Self> {
114        let format = Self::formatting_date_format();
115        let date = date.format(&format).ok()?;
116        Some(Self(
117            Symbol::derivative(
118                &format!("{}{date}{}{price}", Self::OPTIONS, Self::PUT),
119                &format!("{base}-{quote}"),
120            )
121            .expect("must be valid"),
122        ))
123    }
124
125    /// Create a symbol for call options.
126    /// Return `None` if `date` cannot be parsed by the date format.
127    pub fn call(base: &Asset, quote: &Asset, date: Date, price: Decimal) -> Option<Self> {
128        let format = Self::formatting_date_format();
129        let date = date.format(&format).ok()?;
130        Some(Self(
131            Symbol::derivative(
132                &format!("{}{date}{}{price}", Self::OPTIONS, Self::CALL),
133                &format!("{base}-{quote}",),
134            )
135            .expect("must be valid"),
136        ))
137    }
138
139    #[inline]
140    fn parse_price(s: &str) -> Option<Decimal> {
141        Decimal::from_str_exact(s).ok()
142    }
143
144    /// Create a symbol for put options.
145    /// Return `None` if `date` cannot be parsed by the date format.
146    pub fn put_with_str(base: &Asset, quote: &Asset, date: &str, price: &str) -> Option<Self> {
147        let date = Self::parse_date(date)?;
148        let price = Self::parse_price(price)?;
149        Self::put(base, quote, date, price)
150    }
151
152    /// Create a symbol for call options.
153    /// Return `None` if `date` cannot be parsed by the date format.
154    pub fn call_with_str(base: &Asset, quote: &Asset, date: &str, price: &str) -> Option<Self> {
155        let date = Self::parse_date(date)?;
156        let price = Self::parse_price(price)?;
157        Self::call(base, quote, date, price)
158    }
159
160    /// From symbol.
161    pub fn from_symbol(symbol: &Symbol) -> Option<Self> {
162        if symbol.is_spot() {
163            Some(Self(symbol.clone()))
164        } else if let Some((extra, sym)) = symbol.as_derivative() {
165            if !sym.is_ascii() {
166                return None;
167            }
168            let mut parts = sym.split(Self::SEP);
169            Asset::from_str(parts.next()?).ok()?;
170            Asset::from_str(parts.next()?).ok()?;
171            if parts.next().is_some() {
172                return None;
173            }
174            if !extra.is_empty() {
175                let (ty, extra) = extra.split_at(1);
176                match ty {
177                    Self::FUTURES => {
178                        Self::parse_date(extra)?;
179                    }
180                    Self::PERPETUAL => {}
181                    Self::OPTIONS => {
182                        if extra.len() <= 7 {
183                            return None;
184                        }
185                        let (date, opts) = extra.split_at(6);
186                        Self::parse_date(date)?;
187                        let (opts, price) = opts.split_at(1);
188                        Self::parse_price(price)?;
189                        match opts {
190                            Self::PUT => {}
191                            Self::CALL => {}
192                            _ => return None,
193                        };
194                    }
195                    _ => {
196                        return None;
197                    }
198                }
199            }
200            Some(Self(symbol.clone()))
201        } else {
202            None
203        }
204    }
205
206    /// Divide symbol into parts: `(base, quote, type)`.
207    pub fn to_parts(&self) -> (Asset, Asset, SymbolType) {
208        if let Some((base, quote)) = self.0.as_spot() {
209            (base.clone(), quote.clone(), SymbolType::Spot)
210        } else if let Some((extra, symbol)) = self.0.as_derivative() {
211            let mut parts = symbol.split(Self::SEP);
212            let base = parts.next().unwrap();
213            let quote = parts.next().unwrap();
214            let ty = if !extra.is_empty() {
215                let (ty, extra) = extra.split_at(1);
216                match ty {
217                    Self::FUTURES => {
218                        debug_assert_eq!(extra.len(), 6);
219                        SymbolType::Futures(Str::new_inline(extra))
220                    }
221                    Self::PERPETUAL => SymbolType::Perpetual,
222                    Self::OPTIONS => {
223                        let (date, opts) = extra.split_at(6);
224                        let (opts, price) = opts.split_at(1);
225                        let opts = match opts {
226                            Self::PUT => OptionsType::Put(Str::new(price)),
227                            Self::CALL => OptionsType::Call(Str::new(price)),
228                            _ => unreachable!(),
229                        };
230                        SymbolType::Options(Str::new_inline(date), opts)
231                    }
232                    _ => unreachable!(),
233                }
234            } else {
235                SymbolType::Margin
236            };
237            (
238                Asset::from_str(base).unwrap(),
239                Asset::from_str(quote).unwrap(),
240                ty,
241            )
242        } else {
243            unreachable!()
244        }
245    }
246
247    /// Create a [`Instrument`] from [`ExcSymbol`].
248    pub fn to_instrument(&self) -> Instrument {
249        let (base, quote, _) = self.to_parts();
250        Instrument::try_with_symbol(self.0.clone(), &base, &quote).expect("must be valid")
251    }
252}
253
254impl fmt::Display for ExcSymbol {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        write!(f, "{}", self.0)
257    }
258}
259
260/// Parse [`ExcSymbol`] Error.
261#[derive(Debug, Error)]
262pub enum ParseExcSymbolError {
263    /// Parse symbol error.
264    #[error("parse symbol error: {0}")]
265    ParseSymbol(#[from] ParseSymbolError),
266    /// Invalid format.
267    #[error("invalid format")]
268    InvalidFormat,
269}
270
271impl FromStr for ExcSymbol {
272    type Err = ParseExcSymbolError;
273
274    fn from_str(s: &str) -> Result<Self, Self::Err> {
275        let symbol = Symbol::from_str(s)?;
276        Self::from_symbol(&symbol).ok_or(ParseExcSymbolError::InvalidFormat)
277    }
278}
279
280impl Borrow<Symbol> for ExcSymbol {
281    fn borrow(&self) -> &Symbol {
282        &self.0
283    }
284}
285
286impl From<ExcSymbol> for Symbol {
287    fn from(symbol: ExcSymbol) -> Self {
288        symbol.0
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use rust_decimal_macros::dec;
295    use time::macros::date;
296
297    use super::*;
298
299    #[test]
300    fn test_spot() {
301        let symbol: ExcSymbol = "BTC-USDT".parse().unwrap();
302        assert!(symbol.0.is_spot());
303        assert_eq!(
304            symbol.to_parts(),
305            (Asset::BTC, Asset::USDT, SymbolType::Spot)
306        );
307        symbol.to_instrument();
308    }
309
310    #[test]
311    fn test_margin() {
312        let symbol: ExcSymbol = ":BTC-USDT".parse().unwrap();
313        assert!(!symbol.0.is_spot());
314        assert_eq!(
315            symbol.to_parts(),
316            (Asset::BTC, Asset::USDT, SymbolType::Margin)
317        );
318        symbol.to_instrument();
319    }
320
321    #[test]
322    fn test_futures() {
323        let symbol: ExcSymbol = "F221230:BTC-USDT".parse().unwrap();
324        assert!(!symbol.0.is_spot());
325        assert_eq!(
326            symbol.to_parts(),
327            (
328                Asset::BTC,
329                Asset::USDT,
330                SymbolType::Futures(Str::new("221230"))
331            )
332        );
333        symbol.to_instrument();
334    }
335
336    #[test]
337    fn test_perpetual() {
338        let symbol: ExcSymbol = "P:BTC-USDT".parse().unwrap();
339        assert!(!symbol.0.is_spot());
340        assert_eq!(
341            symbol.to_parts(),
342            (Asset::BTC, Asset::USDT, SymbolType::Perpetual,)
343        );
344        symbol.to_instrument();
345    }
346
347    #[test]
348    fn test_call_options() {
349        let symbol: ExcSymbol = "O221230C17000:BTC-USDT".parse().unwrap();
350        assert!(!symbol.0.is_spot());
351        assert_eq!(
352            symbol.to_parts(),
353            (
354                Asset::BTC,
355                Asset::USDT,
356                SymbolType::Options(Str::new("221230"), OptionsType::Call(Str::new("17000"))),
357            )
358        );
359        symbol.to_instrument();
360    }
361
362    #[test]
363    fn test_put_options() {
364        let symbol: ExcSymbol = "O221230P17000:BTC-USDT".parse().unwrap();
365        assert!(!symbol.0.is_spot());
366        assert_eq!(
367            symbol.to_parts(),
368            (
369                Asset::BTC,
370                Asset::USDT,
371                SymbolType::Options(Str::new("221230"), OptionsType::Put(Str::new("17000"))),
372            )
373        );
374        symbol.to_instrument();
375    }
376
377    #[test]
378    fn test_spot_creation() {
379        let symbol = ExcSymbol::spot(&Asset::BTC, &Asset::USDT);
380        assert!(symbol.0.is_spot());
381        assert_eq!(
382            symbol.to_parts(),
383            (Asset::BTC, Asset::USDT, SymbolType::Spot)
384        );
385        symbol.to_instrument();
386    }
387
388    #[test]
389    fn test_margin_creation() {
390        let symbol = ExcSymbol::margin(&Asset::BTC, &Asset::USDT);
391        assert!(!symbol.0.is_spot());
392        assert_eq!(
393            symbol.to_parts(),
394            (Asset::BTC, Asset::USDT, SymbolType::Margin)
395        );
396        symbol.to_instrument();
397    }
398
399    #[test]
400    fn test_futures_creation() {
401        let symbol = ExcSymbol::futures(&Asset::BTC, &Asset::USDT, date!(2022 - 12 - 30)).unwrap();
402        assert!(!symbol.0.is_spot());
403        assert_eq!(
404            symbol.to_parts(),
405            (
406                Asset::BTC,
407                Asset::USDT,
408                SymbolType::Futures(Str::new("221230"))
409            )
410        );
411        symbol.to_instrument();
412    }
413
414    #[test]
415    fn test_perpetual_creation() {
416        let symbol = ExcSymbol::perpetual(&Asset::BTC, &Asset::USDT);
417        assert!(!symbol.0.is_spot());
418        assert_eq!(
419            symbol.to_parts(),
420            (Asset::BTC, Asset::USDT, SymbolType::Perpetual,)
421        );
422        symbol.to_instrument();
423    }
424
425    #[test]
426    fn test_call_options_creation() {
427        let symbol = ExcSymbol::call(
428            &Asset::BTC,
429            &Asset::USDT,
430            date!(2022 - 12 - 30),
431            dec!(17000),
432        )
433        .unwrap();
434        assert!(!symbol.0.is_spot());
435        assert_eq!(
436            symbol.to_parts(),
437            (
438                Asset::BTC,
439                Asset::USDT,
440                SymbolType::Options(Str::new("221230"), OptionsType::Call(Str::new("17000"))),
441            )
442        );
443        symbol.to_instrument();
444    }
445
446    #[test]
447    fn test_put_options_creation() {
448        let symbol = ExcSymbol::put(
449            &Asset::BTC,
450            &Asset::USDT,
451            date!(2022 - 12 - 30),
452            dec!(17000),
453        )
454        .unwrap();
455        assert!(!symbol.0.is_spot());
456        assert_eq!(
457            symbol.to_parts(),
458            (
459                Asset::BTC,
460                Asset::USDT,
461                SymbolType::Options(Str::new("221230"), OptionsType::Put(Str::new("17000"))),
462            )
463        );
464        symbol.to_instrument();
465    }
466}