Skip to main content

polyoxide_venue/
records.rs

1//! The records every venue's market data is read into.
2//!
3//! A record holds what venues share, as fields, and a venue's own facts in its
4//! [`Extensions`]. A field a venue does not publish is an `Option`. Prices and
5//! sizes are [`Decimal`]; a size is in the instrument's native quantity, which
6//! [`Instrument::size_unit`] names, and an amount in the quote currency is a
7//! separate `notional`.
8
9use 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/// Which side of the book an order, or the taker of a trade, is on.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum Side {
21    /// Buying, against the asks.
22    Buy,
23    /// Selling, against the bids.
24    Sell,
25}
26
27/// One price level of a book.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29#[non_exhaustive]
30pub struct Level {
31    /// The price, in the instrument's quote currency.
32    pub price: Decimal,
33    /// The size resting at the price, in the instrument's native quantity.
34    pub size: Decimal,
35}
36
37impl Level {
38    /// A level of `size` at `price`.
39    pub fn new(price: Decimal, size: Decimal) -> Self {
40        Self { price, size }
41    }
42}
43
44/// What an instrument's sizes count.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46#[non_exhaustive]
47pub enum SizeUnit {
48    /// Units of the base asset, such as the coins a coin-quoted contract is
49    /// written in.
50    Base,
51    /// Contracts, each a fixed amount of the underlying the venue defines.
52    Contracts,
53    /// Outcome shares of an event contract, each paying one unit of the
54    /// settlement currency if its outcome happens.
55    Shares,
56}
57
58/// An instrument a venue lists, and the one record that carries its trading
59/// rules: tick size, minimum size and [`SizeUnit`].
60#[derive(Debug, Clone, PartialEq)]
61#[non_exhaustive]
62pub struct Instrument {
63    /// The instrument's key.
64    pub key: MarketKey,
65    /// A name for people: the venue's symbol, or for an outcome of an event
66    /// contract, its market and outcome.
67    pub name: String,
68    /// The asset the instrument's sizes are written in, when the venue names
69    /// one.
70    pub base_asset: Option<String>,
71    /// The currency its prices are quoted in, when the venue names one.
72    pub quote_asset: Option<String>,
73    /// What its sizes count.
74    pub size_unit: SizeUnit,
75    /// The smallest price step, when the venue publishes a fixed one.
76    pub tick_size: Option<Decimal>,
77    /// The smallest order size, in native quantity.
78    pub min_size: Option<Decimal>,
79    /// The smallest size step, in native quantity.
80    pub size_increment: Option<Decimal>,
81    /// The smallest order value, in the quote currency.
82    pub min_notional: Option<Decimal>,
83    /// Whether the venue now takes orders on it that open or add to a
84    /// position. A close-only instrument is not tradable.
85    pub tradable: Option<bool>,
86    /// The venue's own facts.
87    pub extensions: Extensions,
88}
89
90impl Instrument {
91    /// An instrument with its key, name and size unit, and nothing else known.
92    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/// An instrument's best bid and ask at one moment.
110#[derive(Debug, Clone, PartialEq)]
111#[non_exhaustive]
112pub struct Quote {
113    /// The instrument's key.
114    pub key: MarketKey,
115    /// The highest bid, when there is one.
116    pub bid: Option<Level>,
117    /// The lowest ask, when there is one.
118    pub ask: Option<Level>,
119    /// When the venue took it.
120    pub time: Option<UnixMillis>,
121    /// The venue's own facts.
122    pub extensions: Extensions,
123}
124
125impl Quote {
126    /// A quote for `key` with neither side known.
127    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/// An instrument's order book at one moment.
139#[derive(Debug, Clone, PartialEq)]
140#[non_exhaustive]
141pub struct Book {
142    /// The instrument's key.
143    pub key: MarketKey,
144    /// Bids, best (highest price) first.
145    pub bids: Vec<Level>,
146    /// Asks, best (lowest price) first.
147    pub asks: Vec<Level>,
148    /// When the venue took it.
149    pub time: Option<UnixMillis>,
150    /// The venue's own facts.
151    pub extensions: Extensions,
152}
153
154impl Book {
155    /// An empty book for `key`.
156    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/// A trade's id at its venue. Opaque: compare it, never parse it.
168#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
169pub struct TradeId(Arc<str>);
170
171impl TradeId {
172    /// The id `id`.
173    pub fn new(id: impl Into<Arc<str>>) -> Self {
174        Self(id.into())
175    }
176
177    /// The id, as the venue wrote it.
178    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/// One public trade.
190#[derive(Debug, Clone, PartialEq)]
191#[non_exhaustive]
192pub struct Trade {
193    /// The instrument's key.
194    pub key: MarketKey,
195    /// The venue's id for the trade, when it gives one.
196    pub id: Option<TradeId>,
197    /// The price.
198    pub price: Decimal,
199    /// The size, in native quantity.
200    pub size: Decimal,
201    /// The amount, in the quote currency, when the venue publishes it.
202    pub notional: Option<Decimal>,
203    /// The taker's side, when the venue says.
204    pub taker_side: Option<Side>,
205    /// When it traded.
206    pub time: UnixMillis,
207    /// The venue's own facts.
208    pub extensions: Extensions,
209}
210
211impl Trade {
212    /// A trade of `size` at `price`, at `time`.
213    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/// How long a candle spans. A venue serves some or all of these widths, the
228/// ones its [`Candles::intervals`](crate::Candles::intervals) lists, and
229/// handles every variant, since the enum is exhaustive: a new width is a
230/// breaking change that fails to compile at a venue that does not handle it.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
232pub enum CandleInterval {
233    /// One minute.
234    Minute1,
235    /// Five minutes.
236    Minute5,
237    /// Fifteen minutes.
238    Minute15,
239    /// One hour.
240    Hour1,
241    /// Four hours.
242    Hour4,
243    /// One day.
244    Day1,
245    /// One week.
246    Week1,
247}
248
249impl CandleInterval {
250    /// Every width, shortest first.
251    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    /// How long the width is.
262    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/// Which candles to read: their width and the open times they span.
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278#[non_exhaustive]
279pub struct CandleQuery {
280    /// The candles' width.
281    pub interval: CandleInterval,
282    /// The earliest open time.
283    pub start: UnixMillis,
284    /// The latest open time; now, when `None`.
285    pub end: Option<UnixMillis>,
286}
287
288impl CandleQuery {
289    /// Candles of `interval` from `start` to now.
290    pub fn new(interval: CandleInterval, start: UnixMillis) -> Self {
291        Self {
292            interval,
293            start,
294            end: None,
295        }
296    }
297
298    /// Ends the span at `end`.
299    pub fn end(mut self, end: UnixMillis) -> Self {
300        self.end = Some(end);
301        self
302    }
303
304    /// Checks that the query's width is one of `served`, which a venue takes
305    /// from its [`Candles::intervals`](crate::Candles::intervals). Call it
306    /// before the first request, and convert the refusal with `?`.
307    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/// A query asked for a candle width the venue does not serve
320/// ([`CandleQuery::ensure_served`]). An
321/// [`InvalidRequest`](Class::InvalidRequest): the caller's misuse, refused
322/// before anything is sent, and no wait or retry cures it.
323///
324/// Only [`CandleQuery::ensure_served`] builds one, since a field may be added
325/// later and a struct literal is refused when the program is built:
326///
327/// ```compile_fail
328/// use polyoxide_venue::{CandleInterval, UnservedInterval};
329///
330/// fn refuse() -> UnservedInterval {
331///     UnservedInterval {
332///         interval: CandleInterval::Minute5,
333///         served: &[],
334///     }
335/// }
336/// ```
337#[derive(Debug, Clone, PartialEq, Eq)]
338#[non_exhaustive]
339pub struct UnservedInterval {
340    /// The width asked for.
341    pub interval: CandleInterval,
342    /// The widths the venue serves, as its
343    /// [`Candles::intervals`](crate::Candles::intervals) lists them.
344    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/// One candle. A venue that samples one price per interval gives only
376/// [`close`](Self::close).
377#[derive(Debug, Clone, PartialEq)]
378#[non_exhaustive]
379pub struct Candle {
380    /// The instrument's key.
381    pub key: MarketKey,
382    /// The candle's width.
383    pub interval: CandleInterval,
384    /// When it opens.
385    pub open_time: UnixMillis,
386    /// The first price.
387    pub open: Option<Decimal>,
388    /// The highest price.
389    pub high: Option<Decimal>,
390    /// The lowest price.
391    pub low: Option<Decimal>,
392    /// The last price.
393    pub close: Decimal,
394    /// The size traded, in native quantity.
395    pub volume: Option<Decimal>,
396    /// The amount traded, in the quote currency.
397    pub notional: Option<Decimal>,
398    /// How many trades.
399    pub trades: Option<u64>,
400    /// The venue's own facts.
401    pub extensions: Extensions,
402}
403
404impl Candle {
405    /// A candle of `interval` opening at `open_time` that closed at `close`.
406    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/// A perpetual's funding rate: the one now accruing, or one already settled.
429#[derive(Debug, Clone, PartialEq)]
430#[non_exhaustive]
431pub struct FundingRate {
432    /// The instrument's key.
433    pub key: MarketKey,
434    /// The rate, a fraction of position value per funding period, `interval`
435    /// long.
436    pub rate: Decimal,
437    /// The length of the funding period `rate` is for; `None` when the venue
438    /// does not say.
439    pub interval: Option<Duration>,
440    /// When the venue sampled the rate now accruing, or when a settled one
441    /// settled.
442    pub time: UnixMillis,
443    /// When the rate now accruing next settles; `None` for a settled rate, or
444    /// when none is scheduled.
445    pub next_funding: Option<UnixMillis>,
446    /// The venue's own facts.
447    pub extensions: Extensions,
448}
449
450impl FundingRate {
451    /// A rate of `rate` at `time`.
452    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/// An event and every outcome it groups, across its markets.
465#[derive(Debug, Clone, PartialEq)]
466#[non_exhaustive]
467pub struct EventGroup {
468    /// The venue's id for the event.
469    pub id: String,
470    /// The event's title, when it has one.
471    pub title: Option<String>,
472    /// Its outcomes, market by market.
473    pub outcomes: Vec<EventOutcome>,
474    /// The venue's own facts.
475    pub extensions: Extensions,
476}
477
478impl EventGroup {
479    /// The event `id`, with no outcomes yet.
480    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/// One outcome of an event: a key of its own, in one of the event's markets.
491///
492/// Like [`Level`], it is a value inside a record, an [`EventGroup`], and not a
493/// record of its own, so it carries no [`Extensions`].
494#[derive(Debug, Clone, PartialEq, Eq, Hash)]
495#[non_exhaustive]
496pub struct EventOutcome {
497    /// The outcome's key.
498    pub key: MarketKey,
499    /// The venue's id for the market the outcome is in.
500    pub market: String,
501    /// The market's title, when it has one.
502    pub market_title: Option<String>,
503    /// The outcome's label, such as `Yes`.
504    pub label: String,
505}
506
507impl EventOutcome {
508    /// The outcome `label` of `market`, keyed `key`.
509    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/// The venue's answer identifies the key as an instrument it does not list.
520/// A [`VenueRefusal`](Class::VenueRefusal), as the venue's own answer for an
521/// unknown instrument is.
522///
523/// Not every venue says so: where its answer does not identify the instrument
524/// as unlisted, the venue's own refusal comes through with the same class, and
525/// some venues return an empty quote or book. The guarantee is the class, so
526/// match on `class()`, not on this type.
527///
528/// A crate that depends on this one builds it with [`NotListed::new`], since a
529/// field may be added later, and a struct literal is refused when the program
530/// is built:
531///
532/// ```compile_fail
533/// use polyoxide_venue::{MarketKey, NotListed};
534///
535/// fn refuse(key: MarketKey) -> NotListed {
536///     NotListed { key }
537/// }
538/// ```
539#[derive(Debug, Clone, PartialEq, Eq)]
540#[non_exhaustive]
541pub struct NotListed {
542    /// The key asked for.
543    pub key: MarketKey,
544}
545
546impl NotListed {
547    /// The refusal for `key`, an instrument the venue does not list.
548    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}