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#[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#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum OptionsType {
20 Put(Str),
22 Call(Str),
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum SymbolType {
29 Spot,
31 Margin,
33 Futures(Str),
35 Perpetual,
37 Options(Str, OptionsType),
39}
40
41impl ExcSymbol {
42 pub const MARGIN: &'static str = "";
44 pub const FUTURES: &'static str = "F";
46 pub const PERPETUAL: &'static str = "P";
48 pub const OPTIONS: &'static str = "O";
50 pub const PUT: &'static str = "P";
52 pub const CALL: &'static str = "C";
54 pub const SEP: char = '-';
56
57 fn date_format() -> impl Parsable {
59 format_description!("[year][month][day]")
60 }
61
62 fn formatting_date_format() -> impl Formattable {
64 format_description!("[year repr:last_two][month][day]")
65 }
66
67 pub fn spot(base: &Asset, quote: &Asset) -> Self {
69 Self(Symbol::spot(base, quote))
70 }
71
72 pub fn margin(base: &Asset, quote: &Asset) -> Self {
74 Self(Symbol::derivative("", &format!("{base}-{quote}")).expect("must be valid"))
75 }
76
77 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 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 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 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 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 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 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 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 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 pub fn to_instrument(&self) -> Instrument {
249 let (base, quote, _) = self.to_parts();
250 Instrument::try_with_symbol(self.0.clone(), &base, "e).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#[derive(Debug, Error)]
262pub enum ParseExcSymbolError {
263 #[error("parse symbol error: {0}")]
265 ParseSymbol(#[from] ParseSymbolError),
266 #[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}