architect_api/symbology/
market.rs

1//! Markets represent specific trading pairs on a given venue, via a given route.
2//! For example, the market "BTC Crypto/USD*COINBASE/DIRECT" represents the direct
3//! market connection to Coinbase's BTC/USD market.
4
5use super::{Product, ProductId, Route, RouteId, Symbolic, Venue, VenueId};
6use crate::{cpty, marketdata, uuid_val, Amount, Str};
7use anyhow::Result;
8#[cfg(feature = "netidx")]
9use derive::FromValue;
10use derive_more::Display;
11use enum_dispatch::enum_dispatch;
12#[cfg(feature = "netidx")]
13use netidx_derive::Pack;
14use rust_decimal::Decimal;
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17use smallvec::SmallVec;
18use strum_macros::{EnumString, IntoStaticStr};
19use uuid::{uuid, Uuid};
20
21static MARKET_NS: Uuid = uuid!("0bfe858c-a749-43a9-a99e-6d1f31a760ad");
22uuid_val!(MarketId, MARKET_NS);
23
24#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
25#[cfg_attr(feature = "netidx", derive(Pack, FromValue))]
26pub struct Market {
27    pub id: MarketId,
28    pub name: Str,
29    pub kind: MarketKind,
30    pub venue: VenueId,
31    pub route: RouteId,
32    pub exchange_symbol: Str,
33    pub extra_info: MarketInfo,
34}
35
36impl Market {
37    fn new(
38        kind_name: &str,
39        kind: MarketKind,
40        venue: &Venue,
41        route: &Route,
42        exchange_symbol: &str,
43        extra_info: MarketInfo,
44    ) -> Result<Self> {
45        let name = format!("{kind_name}*{}/{}", venue.name, route.name);
46        Ok(Self {
47            id: MarketId::from(&name),
48            name: Str::try_from(name.as_str())?,
49            kind,
50            venue: venue.id,
51            route: route.id,
52            exchange_symbol: Str::try_from(exchange_symbol)?,
53            extra_info,
54        })
55    }
56
57    pub fn exchange(
58        base: &Product,
59        quote: &Product,
60        venue: &Venue,
61        route: &Route,
62        exchange_symbol: &str,
63        extra_info: MarketInfo,
64    ) -> Result<Self> {
65        Self::new(
66            &format!("{}/{}", base.name, quote.name),
67            MarketKind::Exchange(ExchangeMarketKind { base: base.id, quote: quote.id }),
68            venue,
69            route,
70            exchange_symbol,
71            extra_info,
72        )
73    }
74
75    pub fn pool(
76        products: impl Iterator<Item = Product>,
77        venue: &Venue,
78        route: &Route,
79        exchange_symbol: &str,
80        extra_info: MarketInfo,
81    ) -> Result<Self> {
82        let mut pool: SmallVec<[ProductId; 2]> = SmallVec::new();
83        let mut kind_name = String::new();
84        for p in products {
85            kind_name.push_str(p.name.as_str());
86            kind_name.push('/');
87            pool.push(p.id);
88        }
89        Self::new(
90            &kind_name,
91            MarketKind::Pool(PoolMarketKind { products: pool }),
92            venue,
93            route,
94            exchange_symbol,
95            extra_info,
96        )
97    }
98}
99
100impl Symbolic for Market {
101    type Id = MarketId;
102
103    fn type_name() -> &'static str {
104        "market"
105    }
106
107    fn id(&self) -> Self::Id {
108        self.id
109    }
110
111    fn name(&self) -> Str {
112        self.name
113    }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
117#[cfg_attr(feature = "netidx", derive(Pack))]
118#[serde(tag = "type", content = "value")]
119pub enum MarketKind {
120    /// A regular exchange trading pair, e.g. Coinbase BTC/USD
121    Exchange(ExchangeMarketKind),
122    /// An unordered pool of products, e.g. a Uniswap pool or Curve 3-pool
123    /// The type is still ordered for canonical naming purpose
124    Pool(PoolMarketKind),
125    #[cfg_attr(feature = "netidx", pack(other))]
126    Unknown,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
130#[cfg_attr(feature = "netidx", derive(Pack))]
131#[cfg_attr(feature = "juniper", derive(juniper::GraphQLObject))]
132pub struct ExchangeMarketKind {
133    pub base: ProductId,
134    pub quote: ProductId,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
138#[cfg_attr(feature = "netidx", derive(Pack))]
139pub struct PoolMarketKind {
140    pub products: SmallVec<[ProductId; 2]>,
141}
142
143#[cfg_attr(feature = "juniper", juniper::graphql_object)]
144impl PoolMarketKind {
145    pub fn products(&self) -> Vec<ProductId> {
146        self.products.iter().copied().collect()
147    }
148}
149
150/// Cpty-specific info about a market
151#[derive(Debug, Display, Clone, Serialize, Deserialize, JsonSchema)]
152#[cfg_attr(feature = "netidx", derive(Pack))]
153#[enum_dispatch(NormalizedMarketInfo)]
154#[serde(tag = "type", content = "value")]
155#[rustfmt::skip]
156pub enum MarketInfo {
157    #[cfg_attr(feature = "netidx", pack(tag(  0)))] Test(TestMarketInfo),
158    #[cfg_attr(feature = "netidx", pack(tag(  1)))] External(ExternalMarketInfo),
159    #[cfg_attr(feature = "netidx", pack(tag(106)))] B2C2(cpty::b2c2::B2C2MarketInfo),
160    #[cfg_attr(feature = "netidx", pack(tag(114)))] Binance(cpty::binance::BinanceMarketInfo),
161    #[cfg_attr(feature = "netidx", pack(tag(116)))] Bybit(cpty::bybit::BybitMarketInfo),
162    #[cfg_attr(feature = "netidx", pack(tag(112)))] CboeDigital(cpty::cboe_digital::CboeDigitalMarketInfo),
163    #[cfg_attr(feature = "netidx", pack(tag(100)))] Coinbase(cpty::coinbase::CoinbaseMarketInfo),
164    #[cfg_attr(feature = "netidx", pack(tag(111)))] CoinbasePrime(cpty::coinbase_prime::CoinbasePrimeMarketInfo),
165    #[cfg_attr(feature = "netidx", pack(tag(104)))] Cqg(cpty::cqg::CqgMarketInfo),
166    #[cfg_attr(feature = "netidx", pack(tag(113)))] Cumberland(cpty::cumberland::CumberlandMarketInfo),
167    #[cfg_attr(feature = "netidx", pack(tag(110)))] DYDX(cpty::dydx::DYDXMarketInfo),
168    #[cfg_attr(feature = "netidx", pack(tag(105)))] Databento(marketdata::databento::DatabentoMarketInfo),
169    #[cfg_attr(feature = "netidx", pack(tag(115)))] Deltix(cpty::deltix::DeltixMarketInfo),
170    #[cfg_attr(feature = "netidx", pack(tag(101)))] Deribit(cpty::deribit::DeribitMarketInfo),
171    #[cfg_attr(feature = "netidx", pack(tag(109)))] FalconX(cpty::falconx::FalconXMarketInfo),
172    #[cfg_attr(feature = "netidx", pack(tag(108)))] Galaxy(cpty::galaxy::GalaxyMarketInfo),
173    #[cfg_attr(feature = "netidx", pack(tag(102)))] Kraken(cpty::kraken::KrakenMarketInfo),
174    #[cfg_attr(feature = "netidx", pack(tag(103)))] Okx(cpty::okx::OkxMarketInfo),
175    #[cfg_attr(feature = "netidx", pack(tag(107)))] Wintermute(cpty::wintermute::WintermuteMarketInfo),
176}
177
178#[enum_dispatch]
179pub trait NormalizedMarketInfo {
180    /// Return the tick size of the market or 1 if unknown
181    fn tick_size(&self) -> Decimal;
182
183    /// Return the step size of the market
184    fn step_size(&self) -> Decimal;
185
186    /// The minimum quantity per order allowed by the exchange.
187    /// Some exchanges like Coinbase express this in terms of quote currency
188    /// (min market funds) while most others express in terms of base currency.
189    /// Defaults to the step size in base currency.
190    fn min_order_quantity(&self) -> Amount<Decimal, MinOrderQuantityUnit> {
191        return Amount::new(self.step_size(), MinOrderQuantityUnit::Base);
192    }
193
194    /// Return if the market is delisted
195    fn is_delisted(&self) -> bool;
196
197    // CR alee: these should maybe be more marketdata-like
198    // esp. for exchanges where it's calculated live and not daily
199    fn initial_margin(&self) -> Option<Decimal> {
200        None
201    }
202
203    fn maintenance_margin(&self) -> Option<Decimal> {
204        None
205    }
206}
207
208#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
209#[cfg_attr(feature = "netidx", derive(Pack))]
210pub struct ExternalMarketInfo {
211    pub tick_size: Decimal,
212    pub step_size: Decimal,
213    pub min_order_quantity: Decimal,
214    pub min_order_quantity_unit: MinOrderQuantityUnit,
215    pub is_delisted: bool,
216}
217
218impl std::fmt::Display for ExternalMarketInfo {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        write!(f, "{}", serde_json::to_string_pretty(self).unwrap())?;
221        Ok(())
222    }
223}
224
225impl NormalizedMarketInfo for ExternalMarketInfo {
226    fn tick_size(&self) -> Decimal {
227        self.tick_size
228    }
229
230    fn step_size(&self) -> Decimal {
231        self.step_size
232    }
233
234    fn min_order_quantity(&self) -> Amount<Decimal, MinOrderQuantityUnit> {
235        Amount::new(self.min_order_quantity, self.min_order_quantity_unit)
236    }
237
238    fn is_delisted(&self) -> bool {
239        self.is_delisted
240    }
241}
242
243#[derive(
244    Default,
245    Debug,
246    Clone,
247    Copy,
248    EnumString,
249    IntoStaticStr,
250    Serialize,
251    Deserialize,
252    JsonSchema,
253)]
254#[cfg_attr(feature = "juniper", derive(juniper::GraphQLEnum))]
255#[cfg_attr(feature = "netidx", derive(Pack))]
256#[serde(tag = "unit", rename_all = "snake_case")]
257#[strum(serialize_all = "snake_case")]
258pub enum MinOrderQuantityUnit {
259    #[default]
260    Base,
261    Quote,
262}
263
264#[cfg(feature = "postgres")]
265crate::to_sql_str!(MinOrderQuantityUnit);
266
267#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
268#[cfg_attr(feature = "netidx", derive(Pack))]
269pub struct TestMarketInfo {
270    pub tick_size: Decimal,
271    pub step_size: Decimal,
272    pub is_delisted: bool,
273}
274
275impl std::fmt::Display for TestMarketInfo {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        write!(f, "{}", serde_json::to_string_pretty(self).unwrap())?;
278        Ok(())
279    }
280}
281
282impl NormalizedMarketInfo for TestMarketInfo {
283    fn tick_size(&self) -> Decimal {
284        self.tick_size
285    }
286
287    fn step_size(&self) -> Decimal {
288        self.step_size
289    }
290
291    fn is_delisted(&self) -> bool {
292        self.is_delisted
293    }
294}