1use std::error::Error;
10use std::fmt;
11use std::sync::Arc;
12use std::time::Duration;
13
14use rust_decimal::Decimal;
15
16use crate::{Class, Classify, Extensions, MarketKey, UnixMillis};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum Side {
21 Buy,
23 Sell,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[non_exhaustive]
30pub struct Level {
31 pub price: Decimal,
33 pub size: Decimal,
35}
36
37impl Level {
38 pub fn new(price: Decimal, size: Decimal) -> Self {
40 Self { price, size }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46#[non_exhaustive]
47pub enum SizeUnit {
48 Base,
51 Contracts,
53 Shares,
56}
57
58#[derive(Debug, Clone, PartialEq)]
61#[non_exhaustive]
62pub struct Instrument {
63 pub key: MarketKey,
65 pub name: String,
68 pub base_asset: Option<String>,
71 pub quote_asset: Option<String>,
73 pub size_unit: SizeUnit,
75 pub tick_size: Option<Decimal>,
77 pub min_size: Option<Decimal>,
79 pub size_increment: Option<Decimal>,
81 pub min_notional: Option<Decimal>,
83 pub tradable: Option<bool>,
86 pub extensions: Extensions,
88}
89
90impl Instrument {
91 pub fn new(key: MarketKey, name: impl Into<String>, size_unit: SizeUnit) -> Self {
93 Self {
94 key,
95 name: name.into(),
96 base_asset: None,
97 quote_asset: None,
98 size_unit,
99 tick_size: None,
100 min_size: None,
101 size_increment: None,
102 min_notional: None,
103 tradable: None,
104 extensions: Extensions::new(),
105 }
106 }
107}
108
109#[derive(Debug, Clone, PartialEq)]
111#[non_exhaustive]
112pub struct Quote {
113 pub key: MarketKey,
115 pub bid: Option<Level>,
117 pub ask: Option<Level>,
119 pub time: Option<UnixMillis>,
121 pub extensions: Extensions,
123}
124
125impl Quote {
126 pub fn new(key: MarketKey) -> Self {
128 Self {
129 key,
130 bid: None,
131 ask: None,
132 time: None,
133 extensions: Extensions::new(),
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq)]
140#[non_exhaustive]
141pub struct Book {
142 pub key: MarketKey,
144 pub bids: Vec<Level>,
146 pub asks: Vec<Level>,
148 pub time: Option<UnixMillis>,
150 pub extensions: Extensions,
152}
153
154impl Book {
155 pub fn new(key: MarketKey) -> Self {
157 Self {
158 key,
159 bids: Vec::new(),
160 asks: Vec::new(),
161 time: None,
162 extensions: Extensions::new(),
163 }
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
169pub struct TradeId(Arc<str>);
170
171impl TradeId {
172 pub fn new(id: impl Into<Arc<str>>) -> Self {
174 Self(id.into())
175 }
176
177 pub fn as_str(&self) -> &str {
179 &self.0
180 }
181}
182
183impl fmt::Display for TradeId {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 f.write_str(&self.0)
186 }
187}
188
189#[derive(Debug, Clone, PartialEq)]
191#[non_exhaustive]
192pub struct Trade {
193 pub key: MarketKey,
195 pub id: Option<TradeId>,
197 pub price: Decimal,
199 pub size: Decimal,
201 pub notional: Option<Decimal>,
203 pub taker_side: Option<Side>,
205 pub time: UnixMillis,
207 pub extensions: Extensions,
209}
210
211impl Trade {
212 pub fn new(key: MarketKey, price: Decimal, size: Decimal, time: UnixMillis) -> Self {
214 Self {
215 key,
216 id: None,
217 price,
218 size,
219 notional: None,
220 taker_side: None,
221 time,
222 extensions: Extensions::new(),
223 }
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
232pub enum CandleInterval {
233 Minute1,
235 Minute5,
237 Minute15,
239 Hour1,
241 Hour4,
243 Day1,
245 Week1,
247}
248
249impl CandleInterval {
250 pub const ALL: &'static [CandleInterval] = &[
252 Self::Minute1,
253 Self::Minute5,
254 Self::Minute15,
255 Self::Hour1,
256 Self::Hour4,
257 Self::Day1,
258 Self::Week1,
259 ];
260
261 pub fn duration(self) -> Duration {
263 let minutes = match self {
264 Self::Minute1 => 1,
265 Self::Minute5 => 5,
266 Self::Minute15 => 15,
267 Self::Hour1 => 60,
268 Self::Hour4 => 240,
269 Self::Day1 => 1_440,
270 Self::Week1 => 10_080,
271 };
272 Duration::from_secs(minutes * 60)
273 }
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278#[non_exhaustive]
279pub struct CandleQuery {
280 pub interval: CandleInterval,
282 pub start: UnixMillis,
284 pub end: Option<UnixMillis>,
286}
287
288impl CandleQuery {
289 pub fn new(interval: CandleInterval, start: UnixMillis) -> Self {
291 Self {
292 interval,
293 start,
294 end: None,
295 }
296 }
297
298 pub fn end(mut self, end: UnixMillis) -> Self {
300 self.end = Some(end);
301 self
302 }
303
304 pub fn ensure_served(&self, served: &'static [CandleInterval]) -> Result<(), UnservedInterval> {
308 if served.contains(&self.interval) {
309 Ok(())
310 } else {
311 Err(UnservedInterval {
312 interval: self.interval,
313 served,
314 })
315 }
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq)]
338#[non_exhaustive]
339pub struct UnservedInterval {
340 pub interval: CandleInterval,
342 pub served: &'static [CandleInterval],
345}
346
347impl fmt::Display for UnservedInterval {
348 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
349 write!(
350 f,
351 "`{:?}` candles are not served; this venue serves ",
352 self.interval
353 )?;
354 if self.served.is_empty() {
355 return f.write_str("none");
356 }
357 for (index, width) in self.served.iter().enumerate() {
358 if index > 0 {
359 f.write_str(", ")?;
360 }
361 write!(f, "{width:?}")?;
362 }
363 Ok(())
364 }
365}
366
367impl Error for UnservedInterval {}
368
369impl Classify for UnservedInterval {
370 fn class(&self) -> Class {
371 Class::InvalidRequest
372 }
373}
374
375#[derive(Debug, Clone, PartialEq)]
378#[non_exhaustive]
379pub struct Candle {
380 pub key: MarketKey,
382 pub interval: CandleInterval,
384 pub open_time: UnixMillis,
386 pub open: Option<Decimal>,
388 pub high: Option<Decimal>,
390 pub low: Option<Decimal>,
392 pub close: Decimal,
394 pub volume: Option<Decimal>,
396 pub notional: Option<Decimal>,
398 pub trades: Option<u64>,
400 pub extensions: Extensions,
402}
403
404impl Candle {
405 pub fn new(
407 key: MarketKey,
408 interval: CandleInterval,
409 open_time: UnixMillis,
410 close: Decimal,
411 ) -> Self {
412 Self {
413 key,
414 interval,
415 open_time,
416 open: None,
417 high: None,
418 low: None,
419 close,
420 volume: None,
421 notional: None,
422 trades: None,
423 extensions: Extensions::new(),
424 }
425 }
426}
427
428#[derive(Debug, Clone, PartialEq)]
430#[non_exhaustive]
431pub struct FundingRate {
432 pub key: MarketKey,
434 pub rate: Decimal,
437 pub interval: Option<Duration>,
440 pub time: UnixMillis,
443 pub next_funding: Option<UnixMillis>,
446 pub extensions: Extensions,
448}
449
450impl FundingRate {
451 pub fn new(key: MarketKey, rate: Decimal, time: UnixMillis) -> Self {
453 Self {
454 key,
455 rate,
456 interval: None,
457 time,
458 next_funding: None,
459 extensions: Extensions::new(),
460 }
461 }
462}
463
464#[derive(Debug, Clone, PartialEq)]
466#[non_exhaustive]
467pub struct EventGroup {
468 pub id: String,
470 pub title: Option<String>,
472 pub outcomes: Vec<EventOutcome>,
474 pub extensions: Extensions,
476}
477
478impl EventGroup {
479 pub fn new(id: impl Into<String>) -> Self {
481 Self {
482 id: id.into(),
483 title: None,
484 outcomes: Vec::new(),
485 extensions: Extensions::new(),
486 }
487 }
488}
489
490#[derive(Debug, Clone, PartialEq, Eq, Hash)]
495#[non_exhaustive]
496pub struct EventOutcome {
497 pub key: MarketKey,
499 pub market: String,
501 pub market_title: Option<String>,
503 pub label: String,
505}
506
507impl EventOutcome {
508 pub fn new(key: MarketKey, market: impl Into<String>, label: impl Into<String>) -> Self {
510 Self {
511 key,
512 market: market.into(),
513 market_title: None,
514 label: label.into(),
515 }
516 }
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
540#[non_exhaustive]
541pub struct NotListed {
542 pub key: MarketKey,
544}
545
546impl NotListed {
547 pub fn new(key: MarketKey) -> Self {
549 Self { key }
550 }
551}
552
553impl fmt::Display for NotListed {
554 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
555 write!(f, "{} is not listed", self.key)
556 }
557}
558
559impl Error for NotListed {}
560
561impl Classify for NotListed {
562 fn class(&self) -> Class {
563 Class::VenueRefusal { code: None }
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use std::borrow::Cow;
570
571 use super::*;
572 use crate::{KeySpace, ProductClass, ProductId, VenueId};
573
574 struct Spot;
575
576 impl KeySpace for Spot {
577 const VENUE: VenueId = VenueId::from_static("acme");
578 const PRODUCT: ProductId = ProductId::from_static("spot");
579 const CLASS: ProductClass = ProductClass::PerpetualFuture;
580 type Id = String;
581
582 fn parse_id(text: &str) -> Result<String, String> {
583 Ok(text.to_owned())
584 }
585
586 fn id_text(id: &String) -> Cow<'_, str> {
587 Cow::Borrowed(id)
588 }
589 }
590
591 #[derive(Debug, Clone, PartialEq)]
592 struct SpotInstrumentExt {
593 lot: u32,
594 }
595
596 fn instrument(lot: u32) -> Instrument {
597 let mut instrument = Instrument::new(Spot::key(&"ABC".to_owned()), "ABC", SizeUnit::Base);
598 instrument.tick_size = Some(Decimal::new(1, 2));
599 instrument
600 .extensions
601 .insert(SpotInstrumentExt { lot })
602 .unwrap();
603 instrument
604 }
605
606 #[test]
607 fn records_are_clone_debug_and_partial_eq() {
608 fn holds<T: Clone + std::fmt::Debug + PartialEq>() {}
609 holds::<Instrument>();
610 holds::<Level>();
611 holds::<Side>();
612 holds::<SizeUnit>();
613 holds::<Quote>();
614 holds::<Book>();
615 holds::<Trade>();
616 holds::<Candle>();
617 holds::<FundingRate>();
618 holds::<EventGroup>();
619 holds::<EventOutcome>();
620 }
621
622 #[test]
623 fn each_record_carries_extensions_into_its_equality() {
624 #[derive(Debug, Clone, PartialEq)]
625 struct Venue(u8);
626
627 let key = Spot::key(&"ABC".to_owned());
628 let time = UnixMillis(1);
629 let mut quote = Quote::new(key.clone());
630 quote.extensions.insert(Venue(1)).unwrap();
631 let mut other = Quote::new(key.clone());
632 other.extensions.insert(Venue(2)).unwrap();
633 assert_ne!(quote, other);
634
635 let mut trade = Trade::new(key.clone(), Decimal::ONE, Decimal::ONE, time);
636 let plain = trade.clone();
637 trade.extensions.insert(Venue(1)).unwrap();
638 assert_ne!(trade, plain);
639
640 let mut candle = Candle::new(key.clone(), CandleInterval::Hour1, time, Decimal::ONE);
641 let plain = candle.clone();
642 candle.extensions.insert(Venue(1)).unwrap();
643 assert_ne!(candle, plain);
644
645 let mut rate = FundingRate::new(key.clone(), Decimal::ONE, time);
646 let plain = rate.clone();
647 rate.extensions.insert(Venue(1)).unwrap();
648 assert_ne!(rate, plain);
649
650 let mut book = Book::new(key);
651 let plain = book.clone();
652 book.extensions.insert(Venue(1)).unwrap();
653 assert_ne!(book, plain);
654
655 let mut group = EventGroup::new("1");
656 let plain = group.clone();
657 group.extensions.insert(Venue(1)).unwrap();
658 assert_ne!(group, plain);
659 }
660
661 #[test]
662 fn each_candle_interval_has_its_duration_in_order() {
663 let minutes: Vec<u64> = CandleInterval::ALL
664 .iter()
665 .map(|interval| interval.duration().as_secs() / 60)
666 .collect();
667 assert_eq!(minutes, [1, 5, 15, 60, 240, 1_440, 10_080]);
668 }
669
670 #[test]
671 fn a_candle_query_ends_now_unless_told() {
672 let query = CandleQuery::new(CandleInterval::Hour1, UnixMillis(5));
673 assert_eq!(query.end, None);
674 assert_eq!(query.end(UnixMillis(9)).end, Some(UnixMillis(9)));
675 }
676
677 #[test]
678 fn a_trade_id_is_opaque_text() {
679 let id = TradeId::new("8353866920580050");
680 assert_eq!(id.as_str(), "8353866920580050");
681 assert_eq!(id.to_string(), "8353866920580050");
682 assert_eq!(id, TradeId::new(String::from("8353866920580050")));
683 }
684
685 #[test]
686 fn an_unlisted_key_is_a_venue_refusal_that_names_it() {
687 let err = NotListed::new(Spot::key(&"ABC".to_owned()));
688 assert_eq!(err.to_string(), "acme.spot:ABC is not listed");
689 assert_eq!(err.class(), Class::VenueRefusal { code: None });
690 assert!(!err.is_retriable());
691 }
692
693 #[test]
694 fn two_records_differing_only_in_one_extension_value_are_unequal() {
695 assert_eq!(instrument(1), instrument(1));
696 assert_ne!(instrument(1), instrument(2));
697 }
698
699 #[test]
700 fn a_new_instrument_knows_only_what_it_was_given() {
701 let key = Spot::key(&"ABC".to_owned());
702 let instrument = Instrument::new(key.clone(), "ABC", SizeUnit::Shares);
703 assert_eq!(instrument.key, key);
704 assert_eq!(instrument.name, "ABC");
705 assert_eq!(instrument.base_asset, None);
706 assert_eq!(instrument.quote_asset, None);
707 assert_eq!(instrument.size_unit, SizeUnit::Shares);
708 assert_eq!(instrument.tick_size, None);
709 assert_eq!(instrument.min_size, None);
710 assert_eq!(instrument.size_increment, None);
711 assert_eq!(instrument.min_notional, None);
712 assert_eq!(instrument.tradable, None);
713 assert!(instrument.extensions.is_empty());
714 }
715
716 #[test]
717 fn a_level_holds_its_price_and_native_size() {
718 let level = Level::new(Decimal::new(84_1255, 1), Decimal::new(14_663, 3));
719 assert_eq!(level.price.to_string(), "84125.5");
720 assert_eq!(level.size.to_string(), "14.663");
721 }
722}