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}