|
|
@@ -0,0 +1,271 @@ |
|
|
|
use std::num::{NonZeroU64, NonZeroU8, NonZeroI32}; |
|
|
|
use std::mem::size_of; |
|
|
|
use std::convert::TryFrom; |
|
|
|
use serde::{Serialize, Deserialize}; |
|
|
|
use markets::crypto::{Exchange, Currency, Ticker, Side}; |
|
|
|
|
|
|
|
mod try_from_u8 { |
|
|
|
use std::convert::TryFrom; |
|
|
|
use std::fmt; |
|
|
|
use std::marker::PhantomData; |
|
|
|
use serde::{Serializer, Deserializer}; |
|
|
|
use serde::de::Visitor; |
|
|
|
use serde::ser::Error as SerError; |
|
|
|
|
|
|
|
struct V<T>(PhantomData<T>); |
|
|
|
|
|
|
|
impl<'de, T> Visitor<'de> for V<T> |
|
|
|
where T: TryFrom<u8> |
|
|
|
{ |
|
|
|
type Value = T; |
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
|
|
|
formatter.write_str("an integer code between 1-255") |
|
|
|
} |
|
|
|
|
|
|
|
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E> |
|
|
|
where E: serde::de::Error, |
|
|
|
{ |
|
|
|
match T::try_from(v) { |
|
|
|
Ok(v) => Ok(v), |
|
|
|
Err(_) => { |
|
|
|
Err(serde::de::Error::custom("Invalid code")) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> |
|
|
|
where E: serde::de::Error, |
|
|
|
{ |
|
|
|
if v > 255 { |
|
|
|
return Err(serde::de::Error::custom("Value greater than 255")) |
|
|
|
} |
|
|
|
match T::try_from(v as u8) { |
|
|
|
Ok(v) => Ok(v), |
|
|
|
Err(_) => { |
|
|
|
Err(serde::de::Error::custom("Invalid code")) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error> |
|
|
|
where D: Deserializer<'de>, |
|
|
|
T: TryFrom<u8> |
|
|
|
{ |
|
|
|
deserializer.deserialize_u8(V(PhantomData)) |
|
|
|
} |
|
|
|
|
|
|
|
pub fn serialize<S, T>(item: &T, serializer: S) -> Result<S::Ok, S::Error> |
|
|
|
where S: Serializer, |
|
|
|
T: Copy, |
|
|
|
u8: From<T> |
|
|
|
{ |
|
|
|
match u8::from(*item) { |
|
|
|
0 => Err(S::Error::custom("not implemented: no code for variant or value")), |
|
|
|
x => serializer.serialize_u8(x) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)] |
|
|
|
pub struct Trade { |
|
|
|
pub time: u64, |
|
|
|
#[serde(with = "try_from_u8")] |
|
|
|
pub exch: Exchange, |
|
|
|
#[serde(with = "try_from_u8")] |
|
|
|
pub ticker: Ticker, |
|
|
|
pub price: f64, |
|
|
|
pub amount: f64, |
|
|
|
pub side: Option<Side>, |
|
|
|
pub server_time: Option<NonZeroI32>, |
|
|
|
} |
|
|
|
|
|
|
|
/// Represents the serialized form of a trades row |
|
|
|
/// |
|
|
|
/// ```console,ignore |
|
|
|
/// 1 2 3 |
|
|
|
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 |
|
|
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
|
|
|
/// |e|b|q|s| srvtm | time: u64 | price: f64 | amount: f64 | |
|
|
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
|
|
|
/// | | | | | |
|
|
|
/// | | | | | |
|
|
|
/// | | | | | |
|
|
|
/// | | | | -> server_time: Option<i32> - 0=None, other=nano offset from `time` |
|
|
|
/// | | | | |
|
|
|
/// | | | -> side: Option<Side> - 0=None, 1=Bid, 2=Ask |
|
|
|
/// | | | |
|
|
|
/// | | -> quote: Currency - see markets::crypto for u8 <-> currency codes |
|
|
|
/// | | |
|
|
|
/// | -> base: Currency - see markets::crypto for u8 <-> currency codes |
|
|
|
/// | |
|
|
|
/// -> exch: Exchange - see markets::crypto for u8 <-> exchange codes |
|
|
|
/// |
|
|
|
/// ``` |
|
|
|
/// |
|
|
|
#[derive(Debug, Clone)] |
|
|
|
pub struct PackedTrade { |
|
|
|
pub exch: u8, |
|
|
|
pub base: u8, |
|
|
|
pub quote: u8, |
|
|
|
|
|
|
|
/// 0=None |
|
|
|
pub side: u8, |
|
|
|
|
|
|
|
/// relative offset from `time`; 0=None |
|
|
|
pub server_time: i32, |
|
|
|
|
|
|
|
pub time: u64, |
|
|
|
pub price: f64, |
|
|
|
pub amount: f64, |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Debug, Clone)] |
|
|
|
pub struct ParseError(Box<String>); |
|
|
|
|
|
|
|
/// Pull out individual fields on demand from the serialized bytes of a PackedTrade |
|
|
|
#[repr(align(32))] |
|
|
|
pub struct PackedTradeData<'a>(&'a [u8]); |
|
|
|
|
|
|
|
impl<'a> PackedTradeData<'a> { |
|
|
|
|
|
|
|
const EXCH_OFFSET : usize = 0; |
|
|
|
const BASE_OFFSET : usize = 1; |
|
|
|
const QUOTE_OFFSET : usize = 2; |
|
|
|
const SIDE_OFFSET : usize = 3; |
|
|
|
const SERVER_TIME_OFFSET : usize = 4; |
|
|
|
const TIME_OFFSET : usize = 8; |
|
|
|
const PRICE_OFFSET : usize = 16; |
|
|
|
const AMOUNT_OFFSET : usize = 24; |
|
|
|
|
|
|
|
#[inline] |
|
|
|
pub fn exch(&self) -> Result<Exchange, markets::crypto::Error> { |
|
|
|
Exchange::try_from(self.0[Self::EXCH_OFFSET]) |
|
|
|
} |
|
|
|
|
|
|
|
#[inline] |
|
|
|
pub fn base(&self) -> Result<Currency, markets::crypto::Error> { |
|
|
|
Currency::try_from(self.0[Self::BASE_OFFSET]) |
|
|
|
} |
|
|
|
|
|
|
|
#[inline] |
|
|
|
pub fn quote(&self) -> Result<Currency, markets::crypto::Error> { |
|
|
|
Currency::try_from(self.0[Self::QUOTE_OFFSET]) |
|
|
|
} |
|
|
|
|
|
|
|
#[inline] |
|
|
|
pub fn side(&self) -> Result<Option<Side>, markets::crypto::Error> { |
|
|
|
match self.0[Self::SIDE_OFFSET] { |
|
|
|
0 => Ok(None), |
|
|
|
other => Ok(Some(Side::try_from(other)?)), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[inline] |
|
|
|
pub fn time(&self) -> Result<u64, ParseError> { |
|
|
|
atoi::atoi(&self.0[Self::TIME_OFFSET..(Self::TIME_OFFSET + 8)]) |
|
|
|
.ok_or_else(|| { |
|
|
|
ParseError(Box::new(format!("failed to parse integer: '{}'", |
|
|
|
std::str::from_utf8(&self.0[Self::TIME_OFFSET..(Self::TIME_OFFSET + 8)]).unwrap_or("uft8 error") |
|
|
|
))) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
#[inline] |
|
|
|
pub fn price(&self) -> Result<f64, lexical::Error> { |
|
|
|
lexical::parse(&self.0[Self::PRICE_OFFSET..(Self::PRICE_OFFSET + 8)]) |
|
|
|
} |
|
|
|
|
|
|
|
#[inline] |
|
|
|
pub fn amount(&self) -> Result<f64, lexical::Error> { |
|
|
|
lexical::parse(&self.0[Self::AMOUNT_OFFSET..(Self::AMOUNT_OFFSET + 8)]) |
|
|
|
} |
|
|
|
|
|
|
|
#[inline] |
|
|
|
pub fn server_time(&self) -> Result<Option<u64>, ParseError> { |
|
|
|
let st: i32 = |
|
|
|
atoi::atoi(&self.0[Self::SERVER_TIME_OFFSET..(Self::SERVER_TIME_OFFSET + 4)]) |
|
|
|
.ok_or_else(|| { |
|
|
|
ParseError(Box::new(format!("failed to parse integer: '{}'", |
|
|
|
std::str::from_utf8(&self.0[Self::SERVER_TIME_OFFSET..(Self::SERVER_TIME_OFFSET + 4)]).unwrap_or("uft8 error") |
|
|
|
))) |
|
|
|
})?; |
|
|
|
match st { |
|
|
|
0 => Ok(None), |
|
|
|
|
|
|
|
x @ std::i32::MIN .. 0 => Ok(Some(self.time()? - x.abs() as u64)), |
|
|
|
|
|
|
|
x @ 1 ..= std::i32::MAX => Ok(Some(self.time()? + x as u64)), |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[allow(unused)] |
|
|
|
#[cfg(test)] |
|
|
|
mod tests { |
|
|
|
use super::*; |
|
|
|
use markets::{e, t, c}; |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn verify_packed_trade_is_32_bytes() { |
|
|
|
assert_eq!(size_of::<PackedTrade>(), 32); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn check_bincode_serialized_size() { |
|
|
|
let trade = Trade { |
|
|
|
time: 1586996977191449698, |
|
|
|
exch: e!(bmex), |
|
|
|
ticker: t!(btc-usd), |
|
|
|
price: 1.234, |
|
|
|
amount: 4.567, |
|
|
|
side: None, |
|
|
|
//server_time: NonZeroU64::new(1586996977191449698 + 1_000_000), |
|
|
|
server_time: NonZeroI32::new(1_000_000), |
|
|
|
//server_time: Some(1586996977191449698 + 1_000_000), |
|
|
|
}; |
|
|
|
|
|
|
|
/* |
|
|
|
let packed = PackedTrade { |
|
|
|
time: trade.time, |
|
|
|
exch: u8::from(trade.exch), |
|
|
|
base: u8::from(trade.ticker.base), |
|
|
|
quote: u8::from(trade.ticker.quote), |
|
|
|
amount: trade.amount, |
|
|
|
price: trade.price, |
|
|
|
//side: trade.side.and_then(|s| NonZeroU8::new(u8::from(s))), |
|
|
|
//server_time: NonZeroI32::new(1_000_000), |
|
|
|
side: trade.side.map(|s| u8::from(s)).unwrap_or(0), |
|
|
|
server_time: 1_000_000, |
|
|
|
|
|
|
|
}; |
|
|
|
*/ |
|
|
|
|
|
|
|
assert_eq!(size_of::<Trade>(), 32); |
|
|
|
|
|
|
|
//assert_eq!(serde_json::to_string(&trade).unwrap().len(), 32); |
|
|
|
assert_eq!(bincode::serialized_size(&trade).unwrap(), 32); |
|
|
|
} |
|
|
|
|
|
|
|
#[test] |
|
|
|
fn example_of_36_byte_trades_struct_without_the_offset_i32() { |
|
|
|
#[repr(packed)] |
|
|
|
pub struct Trade36 { |
|
|
|
pub exch: Exchange, |
|
|
|
pub ticker: Ticker, |
|
|
|
pub side: Option<Side>, |
|
|
|
|
|
|
|
pub time: u64, |
|
|
|
pub price: f64, |
|
|
|
pub amount: f64, |
|
|
|
|
|
|
|
pub server_time: Option<NonZeroU64>, |
|
|
|
} |
|
|
|
|
|
|
|
assert_eq!(size_of::<Trade36>(), 36); |
|
|
|
} |
|
|
|
} |
|
|
|
|