Skip to main content

polyoxide_venue/
market.rs

1//! The traits a venue's products implement, so market data reads the same on
2//! every venue.
3//!
4//! Every product implements [`MarketData`]. What only some products offer is a
5//! separate trait, implemented only where offered: [`Instruments`] (the whole
6//! listing, where one request or a few return it), [`Trades`], [`Candles`],
7//! [`Funding`] and [`EventGroups`]. No method answers "unsupported": a product
8//! that cannot do something does not implement its trait.
9//!
10//! A class bundle names what every product of a [`ProductClass`](crate::ProductClass)
11//! offers: [`PerpetualMarket`] is [`MarketData`] and [`Funding`],
12//! [`EventContractMarket`] is [`MarketData`] and [`EventGroups`], and
13//! [`SpotMarket`] is [`MarketData`] alone. Each is implemented for every type
14//! that implements its members.
15//!
16//! Each method returns a [`ClassifiedError`], built with `?` from the module's
17//! own error; the native clients keep their typed errors. A key of another
18//! product is a [`KeyError`](crate::KeyError), an
19//! [`InvalidRequest`](crate::Class::InvalidRequest).
20//!
21//! Each trait has a `Dyn` wrapper, so one value can be held as, for example,
22//! `Arc<DynMarketData<'static>>` whichever venue it reads:
23//!
24//! ```
25//! use std::sync::Arc;
26//!
27//! use polyoxide_venue::{
28//!     Book, ClassifiedError, DynMarketData, Instrument, MarketData, MarketKey, Quote,
29//! };
30//!
31//! struct Closed;
32//!
33//! impl MarketData for Closed {
34//!     async fn instrument(&self, key: &MarketKey) -> Result<Instrument, ClassifiedError> {
35//!         Err(polyoxide_venue::NotListed::new(key.clone()).into())
36//!     }
37//!
38//!     async fn quote(&self, key: &MarketKey) -> Result<Quote, ClassifiedError> {
39//!         Ok(Quote::new(key.clone()))
40//!     }
41//!
42//!     async fn book(&self, key: &MarketKey) -> Result<Book, ClassifiedError> {
43//!         Ok(Book::new(key.clone()))
44//!     }
45//! }
46//!
47//! let venue: Arc<DynMarketData<'static>> = DynMarketData::new_arc(Closed);
48//! # let _ = venue;
49//! ```
50
51use std::future::Future;
52
53use crate::{
54    Book, Candle, CandleQuery, ClassifiedError, EventGroup, FundingRate, Instrument, MarketKey,
55    Quote, Trade, UnixMillis,
56};
57
58/// What every product implements: its instruments, quotes and books.
59#[dynosaur::dynosaur(pub DynMarketData = dyn(box) MarketData)]
60pub trait MarketData: Send + Sync {
61    /// The instrument `key` names.
62    ///
63    /// For a key the venue does not list, the error's class is
64    /// [`VenueRefusal`](crate::Class::VenueRefusal): a [`NotListed`](crate::NotListed)
65    /// when the venue's answer identifies the instrument as unlisted, and
66    /// otherwise the venue's own refusal. This is how to confirm a listing.
67    /// Match on `class()`, not on the type.
68    fn instrument(
69        &self,
70        key: &MarketKey,
71    ) -> impl Future<Output = Result<Instrument, ClassifiedError>> + Send;
72
73    /// The best bid and ask now.
74    ///
75    /// A key the venue does not list is a
76    /// [`VenueRefusal`](crate::Class::VenueRefusal) by its class, and so is a
77    /// listed instrument with nothing on its book, at a venue that answers that
78    /// with a refusal. A venue that cannot tell an unlisted instrument from one
79    /// with nothing quoted returns an empty quote instead. Use
80    /// [`instrument`](MarketData::instrument) to confirm a listing.
81    fn quote(&self, key: &MarketKey)
82        -> impl Future<Output = Result<Quote, ClassifiedError>> + Send;
83
84    /// The order book now, as deep as the venue serves it by default.
85    ///
86    /// A key the venue does not list is a
87    /// [`VenueRefusal`](crate::Class::VenueRefusal) by its class, and so is a
88    /// listed instrument with nothing on its book, at a venue that answers that
89    /// with a refusal. A venue that cannot tell an unlisted instrument from one
90    /// with nothing on its book returns an empty book instead. Use
91    /// [`instrument`](MarketData::instrument) to confirm a listing.
92    fn book(&self, key: &MarketKey) -> impl Future<Output = Result<Book, ClassifiedError>> + Send;
93}
94
95/// Every instrument a product lists, where one request or a few return them.
96#[dynosaur::dynosaur(pub DynInstruments = dyn(box) Instruments)]
97pub trait Instruments: Send + Sync {
98    /// Every instrument the product lists.
99    fn instruments(&self) -> impl Future<Output = Result<Vec<Instrument>, ClassifiedError>> + Send;
100}
101
102/// Public trades.
103#[dynosaur::dynosaur(pub DynTrades = dyn(box) Trades)]
104pub trait Trades: Send + Sync {
105    /// Recent public trades, oldest first: as many as the venue returns in
106    /// one request.
107    fn trades(
108        &self,
109        key: &MarketKey,
110    ) -> impl Future<Output = Result<Vec<Trade>, ClassifiedError>> + Send;
111}
112
113/// Candles.
114#[dynosaur::dynosaur(pub DynCandles = dyn(box) Candles)]
115pub trait Candles: Send + Sync {
116    /// Candles opening between `query.start` and `query.end`, oldest first:
117    /// as many as the venue returns in one request. Read on from the last
118    /// candle's open time for more.
119    ///
120    /// A venue may also bound the span one request covers (its module's docs
121    /// give the bound), so an empty answer before `query.end` does not mean
122    /// there are none later: read on from the end of the span the venue
123    /// covered.
124    ///
125    /// A query whose `interval` is not in [`intervals`](Self::intervals) is
126    /// refused as an [`InvalidRequest`](crate::Class::InvalidRequest) with
127    /// nothing sent. An implementation calls
128    /// [`CandleQuery::ensure_served`](crate::CandleQuery::ensure_served) with
129    /// `self.intervals()` before its first request and converts the
130    /// [`UnservedInterval`](crate::UnservedInterval) with `?`.
131    fn candles(
132        &self,
133        key: &MarketKey,
134        query: &CandleQuery,
135    ) -> impl Future<Output = Result<Vec<Candle>, ClassifiedError>> + Send;
136
137    /// The candle widths this product serves, shortest first. Every width
138    /// ([`CandleInterval::ALL`](crate::CandleInterval::ALL)) unless the
139    /// implementation says otherwise; a product that serves only some
140    /// overrides it, and its module's docs list them.
141    fn intervals(&self) -> &'static [crate::CandleInterval] {
142        crate::CandleInterval::ALL
143    }
144}
145
146/// A perpetual's funding.
147#[dynosaur::dynosaur(pub DynFunding = dyn(box) Funding)]
148pub trait Funding: Send + Sync {
149    /// The rate now accruing, when it next settles, and the length of the
150    /// funding period it is for.
151    fn funding_rate(
152        &self,
153        key: &MarketKey,
154    ) -> impl Future<Output = Result<FundingRate, ClassifiedError>> + Send;
155
156    /// Settled rates from `start`, oldest first: as many as the venue returns
157    /// in one request. Each carries the instrument's current funding interval,
158    /// as a venue keeps no history of it.
159    ///
160    /// A venue may also bound the span one request covers (its module's docs
161    /// give the bound), so an empty answer before now does not mean there are
162    /// none later: read on from the end of the span the venue covered.
163    fn funding_history(
164        &self,
165        key: &MarketKey,
166        start: UnixMillis,
167    ) -> impl Future<Output = Result<Vec<FundingRate>, ClassifiedError>> + Send;
168}
169
170/// Events that group an event-contract venue's outcomes.
171#[dynosaur::dynosaur(pub DynEventGroups = dyn(box) EventGroups)]
172pub trait EventGroups: Send + Sync {
173    /// The event `key`'s outcome belongs to, with every outcome it groups.
174    fn event_group(
175        &self,
176        key: &MarketKey,
177    ) -> impl Future<Output = Result<EventGroup, ClassifiedError>> + Send;
178}
179
180/// What every perpetual-future product offers: [`MarketData`] and
181/// [`Funding`], so a function generic over perpetuals needs one bound.
182///
183/// A class bundle names only what defines its class. A capability that
184/// products of other classes share too, such as [`Candles`], stays out, so a
185/// later product of the class that lacks it still has the bundle. The bundle
186/// is implemented for every type that implements its members, and it has no
187/// `Dyn` wrapper: code that holds trait objects holds one per member.
188///
189/// ```
190/// use polyoxide_venue::{ClassifiedError, FundingRate, MarketKey, PerpetualMarket, Quote};
191///
192/// /// The quote and the funding rate of one perpetual, on any venue.
193/// async fn quote_and_rate<M: PerpetualMarket>(
194///     market: &M,
195///     key: &MarketKey,
196/// ) -> Result<(Quote, FundingRate), ClassifiedError> {
197///     Ok((market.quote(key).await?, market.funding_rate(key).await?))
198/// }
199/// ```
200pub trait PerpetualMarket: MarketData + Funding {}
201
202impl<T: MarketData + Funding> PerpetualMarket for T {}
203
204/// What every event-contract product offers: [`MarketData`] and
205/// [`EventGroups`], so a function generic over event contracts needs one
206/// bound.
207///
208/// Like [`PerpetualMarket`], it names only what defines its class, is
209/// implemented for every type that implements its members, and has no `Dyn`
210/// wrapper.
211pub trait EventContractMarket: MarketData + EventGroups {}
212
213impl<T: MarketData + EventGroups> EventContractMarket for T {}
214
215/// What every [`ProductClass::Spot`](crate::ProductClass::Spot) product
216/// offers: [`MarketData`], and nothing beyond it, so a function generic over
217/// products that trade the asset itself needs one bound.
218///
219/// Like [`PerpetualMarket`], it names only what defines its class, is
220/// implemented for every type that implements its members, and has no `Dyn`
221/// wrapper. Nothing beyond [`MarketData`] defines the class: a capability its
222/// products share with other classes, such as [`Candles`] or [`Trades`], stays
223/// out, so a product of the class that lacks one still has the bundle.
224///
225/// ```
226/// use polyoxide_venue::{ClassifiedError, MarketKey, Quote, SpotMarket};
227///
228/// /// The quote of one asset, on any venue that trades it outright.
229/// async fn quote_of<M: SpotMarket>(
230///     market: &M,
231///     key: &MarketKey,
232/// ) -> Result<Quote, ClassifiedError> {
233///     market.quote(key).await
234/// }
235/// ```
236///
237/// A type that is not [`MarketData`] is not one:
238///
239/// ```compile_fail
240/// use polyoxide_venue::SpotMarket;
241///
242/// struct Nothing;
243///
244/// fn needs_spot_market<M: SpotMarket>() {}
245///
246/// needs_spot_market::<Nothing>();
247/// ```
248pub trait SpotMarket: MarketData {}
249
250impl<T: MarketData> SpotMarket for T {}
251
252#[cfg(test)]
253mod tests {
254    use std::borrow::Cow;
255    use std::sync::Arc;
256
257    use rust_decimal::Decimal;
258
259    use super::*;
260    use crate::{
261        CandleInterval, Class, EventOutcome, KeySpace, Level, NotListed, ProductClass, ProductId,
262        SizeUnit, VenueId,
263    };
264
265    struct Spot;
266
267    impl KeySpace for Spot {
268        const VENUE: VenueId = VenueId::from_static("acme");
269        const PRODUCT: ProductId = ProductId::from_static("spot");
270        const CLASS: ProductClass = ProductClass::PerpetualFuture;
271        type Id = String;
272
273        fn parse_id(text: &str) -> Result<String, String> {
274            Ok(text.to_owned())
275        }
276
277        fn id_text(id: &String) -> Cow<'_, str> {
278            Cow::Borrowed(id)
279        }
280    }
281
282    /// One instrument, `acme.spot:ABC`, with a one-level book.
283    struct Acme;
284
285    impl Acme {
286        fn check(key: &MarketKey) -> Result<String, ClassifiedError> {
287            let id = Spot::id(key)?;
288            if id != "ABC" {
289                return Err(NotListed::new(key.clone()).into());
290            }
291            Ok(id)
292        }
293    }
294
295    impl MarketData for Acme {
296        async fn instrument(&self, key: &MarketKey) -> Result<Instrument, ClassifiedError> {
297            let id = Self::check(key)?;
298            Ok(Instrument::new(key.clone(), id, SizeUnit::Base))
299        }
300
301        async fn quote(&self, key: &MarketKey) -> Result<Quote, ClassifiedError> {
302            Self::check(key)?;
303            let mut quote = Quote::new(key.clone());
304            quote.bid = Some(Level::new(Decimal::ONE, Decimal::TWO));
305            Ok(quote)
306        }
307
308        async fn book(&self, key: &MarketKey) -> Result<Book, ClassifiedError> {
309            Self::check(key)?;
310            let mut book = Book::new(key.clone());
311            book.bids.push(Level::new(Decimal::ONE, Decimal::TWO));
312            Ok(book)
313        }
314    }
315
316    impl Instruments for Acme {
317        async fn instruments(&self) -> Result<Vec<Instrument>, ClassifiedError> {
318            let key = Spot::key(&"ABC".to_owned());
319            Ok(vec![self.instrument(&key).await?])
320        }
321    }
322
323    impl Trades for Acme {
324        async fn trades(&self, key: &MarketKey) -> Result<Vec<Trade>, ClassifiedError> {
325            Self::check(key)?;
326            Ok(vec![Trade::new(
327                key.clone(),
328                Decimal::ONE,
329                Decimal::ONE,
330                UnixMillis(1),
331            )])
332        }
333    }
334
335    impl Candles for Acme {
336        async fn candles(
337            &self,
338            key: &MarketKey,
339            query: &CandleQuery,
340        ) -> Result<Vec<Candle>, ClassifiedError> {
341            Self::check(key)?;
342            Ok(vec![Candle::new(
343                key.clone(),
344                query.interval,
345                query.start,
346                Decimal::ONE,
347            )])
348        }
349    }
350
351    impl Funding for Acme {
352        async fn funding_rate(&self, key: &MarketKey) -> Result<FundingRate, ClassifiedError> {
353            Self::check(key)?;
354            Ok(FundingRate::new(key.clone(), Decimal::ONE, UnixMillis(1)))
355        }
356
357        async fn funding_history(
358            &self,
359            key: &MarketKey,
360            start: UnixMillis,
361        ) -> Result<Vec<FundingRate>, ClassifiedError> {
362            Self::check(key)?;
363            Ok(vec![FundingRate::new(key.clone(), Decimal::ONE, start)])
364        }
365    }
366
367    impl EventGroups for Acme {
368        async fn event_group(&self, key: &MarketKey) -> Result<EventGroup, ClassifiedError> {
369            Self::check(key)?;
370            let mut group = EventGroup::new("E");
371            group
372                .outcomes
373                .push(EventOutcome::new(key.clone(), "M", "Yes"));
374            Ok(group)
375        }
376    }
377
378    fn abc() -> MarketKey {
379        Spot::key(&"ABC".to_owned())
380    }
381
382    /// Polls a future that never waits to completion.
383    fn ready<F: Future>(future: F) -> F::Output {
384        use std::pin::pin;
385        use std::task::{Context, Poll, Waker};
386
387        let mut future = pin!(future);
388        match future
389            .as_mut()
390            .poll(&mut Context::from_waker(Waker::noop()))
391        {
392            Poll::Ready(output) => output,
393            Poll::Pending => panic!("the test venue never waits"),
394        }
395    }
396
397    #[test]
398    fn every_trait_is_send_and_sync_and_held_through_its_dyn_wrapper() {
399        fn send_sync<T: Send + Sync + ?Sized>() {}
400        send_sync::<DynMarketData<'static>>();
401        send_sync::<DynInstruments<'static>>();
402        send_sync::<DynTrades<'static>>();
403        send_sync::<DynCandles<'static>>();
404        send_sync::<DynFunding<'static>>();
405        send_sync::<DynEventGroups<'static>>();
406
407        let market: Arc<DynMarketData<'static>> = DynMarketData::new_arc(Acme);
408        let listing: Arc<DynInstruments<'static>> = DynInstruments::new_arc(Acme);
409        let trades: Arc<DynTrades<'static>> = DynTrades::new_arc(Acme);
410        let candles: Arc<DynCandles<'static>> = DynCandles::new_arc(Acme);
411        let funding: Arc<DynFunding<'static>> = DynFunding::new_arc(Acme);
412        let groups: Arc<DynEventGroups<'static>> = DynEventGroups::new_arc(Acme);
413
414        let key = abc();
415        assert_eq!(
416            ready(market.quote(&key)).unwrap().bid,
417            Some(Level::new(Decimal::ONE, Decimal::TWO))
418        );
419        assert_eq!(ready(market.book(&key)).unwrap().bids.len(), 1);
420        assert_eq!(ready(market.instrument(&key)).unwrap().key, key);
421        assert_eq!(ready(listing.instruments()).unwrap().len(), 1);
422        assert_eq!(ready(trades.trades(&key)).unwrap().len(), 1);
423        let query = CandleQuery::new(CandleInterval::Hour1, UnixMillis(7));
424        assert_eq!(
425            ready(candles.candles(&key, &query)).unwrap()[0].open_time,
426            UnixMillis(7)
427        );
428        assert!(ready(funding.funding_rate(&key)).is_ok());
429        assert_eq!(
430            ready(funding.funding_history(&key, UnixMillis(3))).unwrap()[0].time,
431            UnixMillis(3)
432        );
433        assert_eq!(
434            ready(groups.event_group(&key)).unwrap().outcomes[0].key,
435            key
436        );
437    }
438
439    #[test]
440    fn a_key_of_another_product_is_an_invalid_request_and_an_unknown_one_a_refusal() {
441        let market: Arc<DynMarketData<'static>> = DynMarketData::new_arc(Acme);
442        struct Other;
443        impl KeySpace for Other {
444            const VENUE: VenueId = VenueId::from_static("other");
445            const PRODUCT: ProductId = ProductId::from_static("spot");
446            const CLASS: ProductClass = ProductClass::PerpetualFuture;
447            type Id = String;
448            fn parse_id(text: &str) -> Result<String, String> {
449                Ok(text.to_owned())
450            }
451            fn id_text(id: &String) -> Cow<'_, str> {
452                Cow::Borrowed(id)
453            }
454        }
455        let foreign = Other::key(&"ABC".to_owned());
456        let err = ready(market.quote(&foreign)).unwrap_err();
457        assert_eq!(err.class, Class::InvalidRequest);
458
459        let unknown = Spot::key(&"XYZ".to_owned());
460        let err = ready(market.book(&unknown)).unwrap_err();
461        assert_eq!(err.class, Class::VenueRefusal { code: None });
462        assert!(err.source.downcast_ref::<NotListed>().is_some());
463    }
464    #[test]
465    fn a_product_with_a_bundles_members_is_held_by_its_one_bound() {
466        fn perpetual<M: PerpetualMarket>(market: &M, key: &MarketKey) -> (Quote, FundingRate) {
467            (
468                ready(market.quote(key)).unwrap(),
469                ready(market.funding_rate(key)).unwrap(),
470            )
471        }
472        fn event_contract<M: EventContractMarket>(
473            market: &M,
474            key: &MarketKey,
475        ) -> (Quote, EventGroup) {
476            (
477                ready(market.quote(key)).unwrap(),
478                ready(market.event_group(key)).unwrap(),
479            )
480        }
481
482        let (quote, rate) = perpetual(&Acme, &abc());
483        assert_eq!(quote.bid, Some(Level::new(Decimal::ONE, Decimal::TWO)));
484        assert_eq!(rate.rate, Decimal::ONE);
485        let (quote, group) = event_contract(&Acme, &abc());
486        assert_eq!(quote.key, abc());
487        assert_eq!(group.outcomes.len(), 1);
488    }
489}