use std::error::Error;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use rust_decimal::Decimal;
use crate::{Class, Classify, Extensions, MarketKey, UnixMillis};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Side {
Buy,
Sell,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Level {
pub price: Decimal,
pub size: Decimal,
}
impl Level {
pub fn new(price: Decimal, size: Decimal) -> Self {
Self { price, size }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SizeUnit {
Base,
Contracts,
Shares,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Instrument {
pub key: MarketKey,
pub name: String,
pub base_asset: Option<String>,
pub quote_asset: Option<String>,
pub size_unit: SizeUnit,
pub tick_size: Option<Decimal>,
pub min_size: Option<Decimal>,
pub size_increment: Option<Decimal>,
pub min_notional: Option<Decimal>,
pub tradable: Option<bool>,
pub extensions: Extensions,
}
impl Instrument {
pub fn new(key: MarketKey, name: impl Into<String>, size_unit: SizeUnit) -> Self {
Self {
key,
name: name.into(),
base_asset: None,
quote_asset: None,
size_unit,
tick_size: None,
min_size: None,
size_increment: None,
min_notional: None,
tradable: None,
extensions: Extensions::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Quote {
pub key: MarketKey,
pub bid: Option<Level>,
pub ask: Option<Level>,
pub time: Option<UnixMillis>,
pub extensions: Extensions,
}
impl Quote {
pub fn new(key: MarketKey) -> Self {
Self {
key,
bid: None,
ask: None,
time: None,
extensions: Extensions::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Book {
pub key: MarketKey,
pub bids: Vec<Level>,
pub asks: Vec<Level>,
pub time: Option<UnixMillis>,
pub extensions: Extensions,
}
impl Book {
pub fn new(key: MarketKey) -> Self {
Self {
key,
bids: Vec::new(),
asks: Vec::new(),
time: None,
extensions: Extensions::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TradeId(Arc<str>);
impl TradeId {
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TradeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Trade {
pub key: MarketKey,
pub id: Option<TradeId>,
pub price: Decimal,
pub size: Decimal,
pub notional: Option<Decimal>,
pub taker_side: Option<Side>,
pub time: UnixMillis,
pub extensions: Extensions,
}
impl Trade {
pub fn new(key: MarketKey, price: Decimal, size: Decimal, time: UnixMillis) -> Self {
Self {
key,
id: None,
price,
size,
notional: None,
taker_side: None,
time,
extensions: Extensions::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum CandleInterval {
Minute1,
Minute5,
Minute15,
Hour1,
Hour4,
Day1,
Week1,
}
impl CandleInterval {
pub const ALL: &'static [CandleInterval] = &[
Self::Minute1,
Self::Minute5,
Self::Minute15,
Self::Hour1,
Self::Hour4,
Self::Day1,
Self::Week1,
];
pub fn duration(self) -> Duration {
let minutes = match self {
Self::Minute1 => 1,
Self::Minute5 => 5,
Self::Minute15 => 15,
Self::Hour1 => 60,
Self::Hour4 => 240,
Self::Day1 => 1_440,
Self::Week1 => 10_080,
};
Duration::from_secs(minutes * 60)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CandleQuery {
pub interval: CandleInterval,
pub start: UnixMillis,
pub end: Option<UnixMillis>,
}
impl CandleQuery {
pub fn new(interval: CandleInterval, start: UnixMillis) -> Self {
Self {
interval,
start,
end: None,
}
}
pub fn end(mut self, end: UnixMillis) -> Self {
self.end = Some(end);
self
}
pub fn ensure_served(&self, served: &'static [CandleInterval]) -> Result<(), UnservedInterval> {
if served.contains(&self.interval) {
Ok(())
} else {
Err(UnservedInterval {
interval: self.interval,
served,
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnservedInterval {
pub interval: CandleInterval,
pub served: &'static [CandleInterval],
}
impl fmt::Display for UnservedInterval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"`{:?}` candles are not served; this venue serves ",
self.interval
)?;
if self.served.is_empty() {
return f.write_str("none");
}
for (index, width) in self.served.iter().enumerate() {
if index > 0 {
f.write_str(", ")?;
}
write!(f, "{width:?}")?;
}
Ok(())
}
}
impl Error for UnservedInterval {}
impl Classify for UnservedInterval {
fn class(&self) -> Class {
Class::InvalidRequest
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Candle {
pub key: MarketKey,
pub interval: CandleInterval,
pub open_time: UnixMillis,
pub open: Option<Decimal>,
pub high: Option<Decimal>,
pub low: Option<Decimal>,
pub close: Decimal,
pub volume: Option<Decimal>,
pub notional: Option<Decimal>,
pub trades: Option<u64>,
pub extensions: Extensions,
}
impl Candle {
pub fn new(
key: MarketKey,
interval: CandleInterval,
open_time: UnixMillis,
close: Decimal,
) -> Self {
Self {
key,
interval,
open_time,
open: None,
high: None,
low: None,
close,
volume: None,
notional: None,
trades: None,
extensions: Extensions::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct FundingRate {
pub key: MarketKey,
pub rate: Decimal,
pub interval: Option<Duration>,
pub time: UnixMillis,
pub next_funding: Option<UnixMillis>,
pub extensions: Extensions,
}
impl FundingRate {
pub fn new(key: MarketKey, rate: Decimal, time: UnixMillis) -> Self {
Self {
key,
rate,
interval: None,
time,
next_funding: None,
extensions: Extensions::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct EventGroup {
pub id: String,
pub title: Option<String>,
pub outcomes: Vec<EventOutcome>,
pub extensions: Extensions,
}
impl EventGroup {
pub fn new(id: impl Into<String>) -> Self {
Self {
id: id.into(),
title: None,
outcomes: Vec::new(),
extensions: Extensions::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct EventOutcome {
pub key: MarketKey,
pub market: String,
pub market_title: Option<String>,
pub label: String,
}
impl EventOutcome {
pub fn new(key: MarketKey, market: impl Into<String>, label: impl Into<String>) -> Self {
Self {
key,
market: market.into(),
market_title: None,
label: label.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct NotListed {
pub key: MarketKey,
}
impl NotListed {
pub fn new(key: MarketKey) -> Self {
Self { key }
}
}
impl fmt::Display for NotListed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} is not listed", self.key)
}
}
impl Error for NotListed {}
impl Classify for NotListed {
fn class(&self) -> Class {
Class::VenueRefusal { code: None }
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::{KeySpace, ProductClass, ProductId, VenueId};
struct Spot;
impl KeySpace for Spot {
const VENUE: VenueId = VenueId::from_static("acme");
const PRODUCT: ProductId = ProductId::from_static("spot");
const CLASS: ProductClass = ProductClass::PerpetualFuture;
type Id = String;
fn parse_id(text: &str) -> Result<String, String> {
Ok(text.to_owned())
}
fn id_text(id: &String) -> Cow<'_, str> {
Cow::Borrowed(id)
}
}
#[derive(Debug, Clone, PartialEq)]
struct SpotInstrumentExt {
lot: u32,
}
fn instrument(lot: u32) -> Instrument {
let mut instrument = Instrument::new(Spot::key(&"ABC".to_owned()), "ABC", SizeUnit::Base);
instrument.tick_size = Some(Decimal::new(1, 2));
instrument
.extensions
.insert(SpotInstrumentExt { lot })
.unwrap();
instrument
}
#[test]
fn records_are_clone_debug_and_partial_eq() {
fn holds<T: Clone + std::fmt::Debug + PartialEq>() {}
holds::<Instrument>();
holds::<Level>();
holds::<Side>();
holds::<SizeUnit>();
holds::<Quote>();
holds::<Book>();
holds::<Trade>();
holds::<Candle>();
holds::<FundingRate>();
holds::<EventGroup>();
holds::<EventOutcome>();
}
#[test]
fn each_record_carries_extensions_into_its_equality() {
#[derive(Debug, Clone, PartialEq)]
struct Venue(u8);
let key = Spot::key(&"ABC".to_owned());
let time = UnixMillis(1);
let mut quote = Quote::new(key.clone());
quote.extensions.insert(Venue(1)).unwrap();
let mut other = Quote::new(key.clone());
other.extensions.insert(Venue(2)).unwrap();
assert_ne!(quote, other);
let mut trade = Trade::new(key.clone(), Decimal::ONE, Decimal::ONE, time);
let plain = trade.clone();
trade.extensions.insert(Venue(1)).unwrap();
assert_ne!(trade, plain);
let mut candle = Candle::new(key.clone(), CandleInterval::Hour1, time, Decimal::ONE);
let plain = candle.clone();
candle.extensions.insert(Venue(1)).unwrap();
assert_ne!(candle, plain);
let mut rate = FundingRate::new(key.clone(), Decimal::ONE, time);
let plain = rate.clone();
rate.extensions.insert(Venue(1)).unwrap();
assert_ne!(rate, plain);
let mut book = Book::new(key);
let plain = book.clone();
book.extensions.insert(Venue(1)).unwrap();
assert_ne!(book, plain);
let mut group = EventGroup::new("1");
let plain = group.clone();
group.extensions.insert(Venue(1)).unwrap();
assert_ne!(group, plain);
}
#[test]
fn each_candle_interval_has_its_duration_in_order() {
let minutes: Vec<u64> = CandleInterval::ALL
.iter()
.map(|interval| interval.duration().as_secs() / 60)
.collect();
assert_eq!(minutes, [1, 5, 15, 60, 240, 1_440, 10_080]);
}
#[test]
fn a_candle_query_ends_now_unless_told() {
let query = CandleQuery::new(CandleInterval::Hour1, UnixMillis(5));
assert_eq!(query.end, None);
assert_eq!(query.end(UnixMillis(9)).end, Some(UnixMillis(9)));
}
#[test]
fn a_trade_id_is_opaque_text() {
let id = TradeId::new("8353866920580050");
assert_eq!(id.as_str(), "8353866920580050");
assert_eq!(id.to_string(), "8353866920580050");
assert_eq!(id, TradeId::new(String::from("8353866920580050")));
}
#[test]
fn an_unlisted_key_is_a_venue_refusal_that_names_it() {
let err = NotListed::new(Spot::key(&"ABC".to_owned()));
assert_eq!(err.to_string(), "acme.spot:ABC is not listed");
assert_eq!(err.class(), Class::VenueRefusal { code: None });
assert!(!err.is_retriable());
}
#[test]
fn two_records_differing_only_in_one_extension_value_are_unequal() {
assert_eq!(instrument(1), instrument(1));
assert_ne!(instrument(1), instrument(2));
}
#[test]
fn a_new_instrument_knows_only_what_it_was_given() {
let key = Spot::key(&"ABC".to_owned());
let instrument = Instrument::new(key.clone(), "ABC", SizeUnit::Shares);
assert_eq!(instrument.key, key);
assert_eq!(instrument.name, "ABC");
assert_eq!(instrument.base_asset, None);
assert_eq!(instrument.quote_asset, None);
assert_eq!(instrument.size_unit, SizeUnit::Shares);
assert_eq!(instrument.tick_size, None);
assert_eq!(instrument.min_size, None);
assert_eq!(instrument.size_increment, None);
assert_eq!(instrument.min_notional, None);
assert_eq!(instrument.tradable, None);
assert!(instrument.extensions.is_empty());
}
#[test]
fn a_level_holds_its_price_and_native_size() {
let level = Level::new(Decimal::new(84_1255, 1), Decimal::new(14_663, 3));
assert_eq!(level.price.to_string(), "84125.5");
assert_eq!(level.size.to_string(), "14.663");
}
}