1use std::time::Duration;
38
39use polyoxide_venue::{
40 Book, Candle, CandleInterval, CandleQuery, Candles, Class, ClassifiedError, Funding,
41 FundingRate, Instrument, Instruments, Level, MarketData, MarketKey, NotListed, Quote, Side,
42 SizeUnit, Trade, TradeId, Trades, UnixMillis,
43};
44use rust_decimal::Decimal;
45
46use crate::usdm::{
47 key,
48 types::{self, ContractType, DepthLimit, Filter, Interval, Symbol, SymbolInfo, SymbolStatus},
49 Usdm,
50};
51
52#[derive(Debug, Clone, PartialEq, Default)]
54#[non_exhaustive]
55pub struct InstrumentExt {
56 pub contract_type: Option<ContractType>,
58 pub status: Option<SymbolStatus>,
60 pub pair: Option<String>,
62 pub margin_asset: Option<String>,
64 pub delivery_date: Option<UnixMillis>,
66 pub onboard_date: Option<UnixMillis>,
68 pub filters: Option<Vec<Filter>>,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
75#[non_exhaustive]
76pub struct QuoteExt {
77 pub last_update_id: Option<u64>,
79 pub event_time: Option<UnixMillis>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85#[non_exhaustive]
86pub struct BookExt {
87 pub last_update_id: Option<u64>,
89 pub event_time: Option<UnixMillis>,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
95#[non_exhaustive]
96pub struct TradeExt {
97 pub first_trade_id: Option<u64>,
99 pub last_trade_id: Option<u64>,
101 pub normal_size: Option<Decimal>,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
107#[non_exhaustive]
108pub struct CandleExt {
109 pub close_time: Option<UnixMillis>,
111 pub taker_buy_volume: Option<Decimal>,
113 pub taker_buy_notional: Option<Decimal>,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
119#[non_exhaustive]
120pub struct FundingExt {
121 pub mark_price: Option<Decimal>,
124 pub index_price: Option<Decimal>,
126 pub estimated_settle_price: Option<Decimal>,
129 pub interest_rate: Option<Decimal>,
131}
132
133fn level(level: &types::Level) -> Level {
134 Level::new(level.price, level.quantity)
135}
136
137fn instrument(info: &SymbolInfo) -> Result<Instrument, ClassifiedError> {
139 let symbol = Symbol::new(info.symbol.as_str())
141 .map_err(|err| ClassifiedError::new(Class::Decode, err))?;
142 let mut instrument = Instrument::new(key::key(&symbol), info.symbol.clone(), SizeUnit::Base);
143 instrument.base_asset = Some(info.base_asset.clone());
144 instrument.quote_asset = Some(info.quote_asset.clone());
145 instrument.tradable = Some(info.status == SymbolStatus::Trading);
146 for filter in &info.filters {
147 match filter {
148 Filter::PriceFilter { tick_size, .. } => instrument.tick_size = Some(*tick_size),
149 Filter::LotSize {
150 min_qty, step_size, ..
151 } => {
152 instrument.min_size = Some(*min_qty);
153 instrument.size_increment = Some(*step_size);
154 }
155 Filter::MinNotional { notional } => instrument.min_notional = Some(*notional),
156 _ => {}
157 }
158 }
159 instrument.extensions = polyoxide_venue::Extensions::of(InstrumentExt {
160 contract_type: Some(info.contract_type.clone()),
161 status: Some(info.status.clone()),
162 pair: Some(info.pair.clone()),
163 margin_asset: Some(info.margin_asset.clone()),
164 delivery_date: Some(UnixMillis(info.delivery_date)),
165 onboard_date: Some(UnixMillis(info.onboard_date)),
166 filters: Some(info.filters.clone()),
167 });
168 Ok(instrument)
169}
170
171fn interval(interval: CandleInterval) -> Interval {
173 match interval {
174 CandleInterval::Minute1 => Interval::M1,
175 CandleInterval::Minute5 => Interval::M5,
176 CandleInterval::Minute15 => Interval::M15,
177 CandleInterval::Hour1 => Interval::H1,
178 CandleInterval::Hour4 => Interval::H4,
179 CandleInterval::Day1 => Interval::D1,
180 CandleInterval::Week1 => Interval::W1,
181 }
182}
183
184impl MarketData for Usdm {
185 async fn instrument(&self, key: &MarketKey) -> Result<Instrument, ClassifiedError> {
186 let symbol = key::symbol(key)?;
187 let info = self.exchange().exchange_info().send().await?;
188 match info
189 .symbols
190 .iter()
191 .find(|row| row.symbol == symbol.as_str())
192 {
193 Some(row) => instrument(row),
194 None => Err(NotListed::new(key.clone()).into()),
195 }
196 }
197
198 async fn quote(&self, key: &MarketKey) -> Result<Quote, ClassifiedError> {
199 let symbol = key::symbol(key)?;
200 let depth = self
201 .market()
202 .depth(&symbol)
203 .limit(DepthLimit::Five)
204 .send()
205 .await?;
206 let mut quote = Quote::new(key.clone());
207 quote.bid = depth.bids.first().map(level);
208 quote.ask = depth.asks.first().map(level);
209 quote.time = Some(UnixMillis(depth.transaction_time));
210 quote.extensions = polyoxide_venue::Extensions::of(QuoteExt {
211 last_update_id: Some(depth.last_update_id),
212 event_time: Some(UnixMillis(depth.event_time)),
213 });
214 Ok(quote)
215 }
216
217 async fn book(&self, key: &MarketKey) -> Result<Book, ClassifiedError> {
218 let symbol = key::symbol(key)?;
219 let depth = self.market().depth(&symbol).send().await?;
220 let mut book = Book::new(key.clone());
221 book.bids = depth.bids.iter().map(level).collect();
222 book.asks = depth.asks.iter().map(level).collect();
223 book.time = Some(UnixMillis(depth.transaction_time));
224 book.extensions = polyoxide_venue::Extensions::of(BookExt {
225 last_update_id: Some(depth.last_update_id),
226 event_time: Some(UnixMillis(depth.event_time)),
227 });
228 Ok(book)
229 }
230}
231
232impl Instruments for Usdm {
233 async fn instruments(&self) -> Result<Vec<Instrument>, ClassifiedError> {
237 let info = self.exchange().exchange_info().send().await?;
238 Ok(info
239 .symbols
240 .iter()
241 .filter_map(|row| match instrument(row) {
242 Ok(instrument) => Some(instrument),
243 Err(err) => {
244 tracing::warn!(symbol = %row.symbol, error = %err, "skipping an exchangeInfo row that does not convert");
245 None
246 }
247 })
248 .collect())
249 }
250}
251
252impl Trades for Usdm {
253 async fn trades(&self, key: &MarketKey) -> Result<Vec<Trade>, ClassifiedError> {
254 let symbol = key::symbol(key)?;
255 let rows = self.market().agg_trades(&symbol).limit(1000).send().await?;
256 Ok(rows
257 .iter()
258 .map(|row| {
259 let mut trade =
260 Trade::new(key.clone(), row.price, row.quantity, UnixMillis(row.time));
261 trade.id = Some(TradeId::new(row.id.to_string()));
262 trade.taker_side = Some(if row.is_buyer_maker {
264 Side::Sell
265 } else {
266 Side::Buy
267 });
268 trade.extensions = polyoxide_venue::Extensions::of(TradeExt {
269 first_trade_id: Some(row.first_trade_id),
270 last_trade_id: Some(row.last_trade_id),
271 normal_size: Some(row.normal_quantity),
272 });
273 trade
274 })
275 .collect())
276 }
277}
278
279impl Candles for Usdm {
280 async fn candles(
281 &self,
282 key: &MarketKey,
283 query: &CandleQuery,
284 ) -> Result<Vec<Candle>, ClassifiedError> {
285 let symbol = key::symbol(key)?;
286 let mut request = self
287 .market()
288 .klines(&symbol, interval(query.interval))
289 .start_time(query.start.0);
290 if let Some(end) = query.end {
291 request = request.end_time(end.0);
292 }
293 let rows = request.limit(500).send().await?;
295 Ok(rows
296 .iter()
297 .map(|row| {
298 let mut candle = Candle::new(
299 key.clone(),
300 query.interval,
301 UnixMillis(row.open_time),
302 row.close,
303 );
304 candle.open = Some(row.open);
305 candle.high = Some(row.high);
306 candle.low = Some(row.low);
307 candle.volume = Some(row.volume);
308 candle.notional = Some(row.quote_volume);
309 candle.trades = Some(row.trade_count);
310 candle.extensions = polyoxide_venue::Extensions::of(CandleExt {
311 close_time: Some(UnixMillis(row.close_time)),
312 taker_buy_volume: Some(row.taker_buy_base_volume),
313 taker_buy_notional: Some(row.taker_buy_quote_volume),
314 });
315 candle
316 })
317 .collect())
318 }
319}
320
321const DEFAULT_FUNDING_INTERVAL: Duration = Duration::from_secs(8 * 60 * 60);
323
324impl Usdm {
325 async fn funding_interval(&self, symbol: &Symbol) -> Result<Duration, ClassifiedError> {
332 let rows = self.exchange().funding_info().send().await?;
333 match rows.iter().find(|row| row.symbol == symbol.as_ref()) {
334 None => Ok(DEFAULT_FUNDING_INTERVAL),
335 Some(row) if row.funding_interval_hours == 0 => Err(ClassifiedError::new(
336 Class::Decode,
337 format!("fundingInfo gives {symbol} a funding interval of 0 hours"),
338 )),
339 Some(row) => Ok(Duration::from_secs(
340 u64::from(row.funding_interval_hours) * 60 * 60,
341 )),
342 }
343 }
344}
345
346impl Funding for Usdm {
347 async fn funding_rate(&self, key: &MarketKey) -> Result<FundingRate, ClassifiedError> {
348 let symbol = key::symbol(key)?;
349 let index = self.market().premium_index(&symbol).send().await?;
350 let interval = self.funding_interval(&symbol).await?;
351 let mut rate =
352 FundingRate::new(key.clone(), index.last_funding_rate, UnixMillis(index.time));
353 rate.interval = Some(interval);
354 rate.next_funding =
356 (index.next_funding_time > 0).then_some(UnixMillis(index.next_funding_time));
357 rate.extensions = polyoxide_venue::Extensions::of(FundingExt {
358 mark_price: Some(index.mark_price),
359 index_price: Some(index.index_price),
360 estimated_settle_price: Some(index.estimated_settle_price),
361 interest_rate: Some(index.interest_rate),
362 });
363 Ok(rate)
364 }
365
366 async fn funding_history(
367 &self,
368 key: &MarketKey,
369 start: UnixMillis,
370 ) -> Result<Vec<FundingRate>, ClassifiedError> {
371 let symbol = key::symbol(key)?;
372 let rows = self
373 .market()
374 .funding_rate()
375 .symbol(&symbol)
376 .start_time(start.0)
377 .limit(1000)
378 .send()
379 .await?;
380 let interval = self.funding_interval(&symbol).await?;
383 Ok(rows
384 .iter()
385 .map(|row| {
386 let mut rate =
387 FundingRate::new(key.clone(), row.funding_rate, UnixMillis(row.funding_time));
388 rate.interval = Some(interval);
389 rate.extensions = polyoxide_venue::Extensions::of(FundingExt {
390 mark_price: row.mark_price,
391 ..FundingExt::default()
392 });
393 rate
394 })
395 .collect())
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn every_candle_interval_is_a_kline_width() {
405 let widths: Vec<&str> = CandleInterval::ALL
406 .iter()
407 .map(|each| interval(*each).as_str())
408 .collect();
409 assert_eq!(widths, ["1m", "5m", "15m", "1h", "4h", "1d", "1w"]);
410 }
411
412 #[test]
413 fn a_row_whose_symbol_no_key_carries_is_a_decode_failure() {
414 let info: types::ExchangeInfo = serde_json::from_str(include_str!(
415 "../../tests/fixtures/usdm/rest/exchange_info.json"
416 ))
417 .unwrap();
418 let mut row = info.symbols[0].clone();
419 row.symbol = "BTC/USDT".to_owned();
420 let err = instrument(&row).unwrap_err();
421 assert_eq!(err.class, Class::Decode);
422 }
423}