use super::{amount::Amount, asset::AssetIdentifier, offer::PriceRatio};
use chrono::prelude::*;
use serde::{de, Deserialize, Deserializer};
#[derive(Debug, Clone)]
pub struct Trade {
id: String,
offer_id: String,
paging_token: String,
ledger_close_time: DateTime<Utc>,
base_account: String,
base_amount: Amount,
base_asset: AssetIdentifier,
counter_amount: Amount,
counter_account: String,
counter_asset: AssetIdentifier,
price: PriceRatio,
seller: Seller,
}
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
pub enum Seller {
Base,
Counter,
}
impl Seller {
pub fn is_base(&self) -> bool {
*self == Seller::Base
}
pub fn is_counter(&self) -> bool {
*self == Seller::Counter
}
}
impl<'de> Deserialize<'de> for Trade {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let rep: TradeIntermediate = TradeIntermediate::deserialize(d)?;
let seller = if rep.base_is_seller {
Seller::Base
} else {
Seller::Counter
};
let base_asset = AssetIdentifier::new(
&rep.base_asset_type,
rep.base_asset_code,
rep.base_asset_issuer,
).map_err(|err| de::Error::custom(&err))?;
let counter_asset = AssetIdentifier::new(
&rep.counter_asset_type,
rep.counter_asset_code,
rep.counter_asset_issuer,
).map_err(|err| de::Error::custom(&err))?;
Ok(Trade {
id: rep.id,
paging_token: rep.paging_token,
ledger_close_time: rep.ledger_close_time,
offer_id: rep.offer_id,
base_account: rep.base_account,
base_asset,
base_amount: rep.base_amount,
counter_account: rep.counter_account,
counter_asset,
counter_amount: rep.counter_amount,
price: PriceRatio::from(rep.price),
seller,
})
}
}
#[derive(Deserialize, Debug)]
struct Price {
n: u64,
d: u64,
}
impl From<Price> for PriceRatio {
fn from(price: Price) -> PriceRatio {
PriceRatio::new(price.n, price.d)
}
}
#[derive(Deserialize, Debug)]
struct TradeIntermediate {
id: String,
paging_token: String,
ledger_close_time: DateTime<Utc>,
offer_id: String,
base_account: String,
base_amount: Amount,
base_asset_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
base_asset_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
base_asset_issuer: Option<String>,
counter_account: String,
counter_amount: Amount,
counter_asset_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
counter_asset_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
counter_asset_issuer: Option<String>,
base_is_seller: bool,
price: Price,
}
impl Trade {
pub fn id(&self) -> &str {
&self.id
}
pub fn paging_token(&self) -> &str {
&self.paging_token
}
pub fn closed_at(&self) -> DateTime<Utc> {
self.ledger_close_time
}
pub fn offer_id(&self) -> &str {
&self.offer_id
}
pub fn base_account(&self) -> &str {
&self.base_account
}
pub fn base_asset(&self) -> &AssetIdentifier {
&self.base_asset
}
pub fn base_amount(&self) -> Amount {
self.base_amount
}
pub fn counter_account(&self) -> &str {
&self.counter_account
}
pub fn counter_asset(&self) -> &AssetIdentifier {
&self.counter_asset
}
pub fn counter_amount(&self) -> Amount {
self.counter_amount
}
pub fn price(&self) -> PriceRatio {
self.price
}
pub fn seller(&self) -> Seller {
self.seller
}
pub fn selling_account(&self) -> &str {
if self.seller().is_base() {
self.base_account()
} else {
self.counter_account()
}
}
}
#[cfg(test)]
mod trade_tests {
use super::*;
use serde_json;
fn trade_json() -> &'static str {
include_str!("../../fixtures/trade.json")
}
#[test]
fn it_parses_into_a_trade() {
let trade: Trade = serde_json::from_str(&trade_json()).unwrap();
assert_eq!(trade.id(), "68836918321750017-0");
assert_eq!(trade.paging_token(), "68836918321750017-0");
assert_eq!(trade.closed_at(), Utc.ymd(2018, 2, 2).and_hms(0, 20, 10));
assert_eq!(trade.offer_id(), "695254");
assert_eq!(
trade.base_account(),
"GBZXCJIUEPDXGHMS64UBJHUVKV6ETWYOVHADLTBXJNJFUC7A7RU5B3GN"
);
assert_eq!(trade.base_amount(), Amount::new(1217566));
assert_eq!(trade.base_asset().code(), "XLM");
assert_eq!(
trade.counter_account(),
"GBHKUQDYXGK5IEYORI7DZMMXANOIEHHOF364LNT4Q7EWPUL7FOO2SP6D"
);
assert_eq!(trade.counter_amount(), Amount::new(199601));
assert_eq!(trade.counter_asset().code(), "SLT");
assert_eq!(trade.price(), PriceRatio::new(10, 61));
assert!(trade.seller().is_base());
assert_eq!(
trade.selling_account(),
"GBZXCJIUEPDXGHMS64UBJHUVKV6ETWYOVHADLTBXJNJFUC7A7RU5B3GN"
)
}
}
#[derive(Clone, Deserialize, Debug)]
pub struct TradeAggregation {
timestamp: u64,
trade_count: u64,
base_volume: Amount,
counter_volume: Amount,
avg: Amount,
high: Amount,
low: Amount,
open: Amount,
close: Amount,
}
impl TradeAggregation {
pub fn started_at(&self) -> DateTime<Utc> {
let secs = self.timestamp / 1000;
let nanos = (self.timestamp % 1000) * 1_000_000;
Utc.timestamp(secs as i64, nanos as u32)
}
pub fn count(&self) -> u64 {
self.trade_count
}
pub fn base_volume(&self) -> Amount {
self.base_volume
}
pub fn counter_volume(&self) -> Amount {
self.counter_volume
}
pub fn average(&self) -> Amount {
self.avg
}
pub fn high(&self) -> Amount {
self.high
}
pub fn low(&self) -> Amount {
self.low
}
pub fn open(&self) -> Amount {
self.open
}
pub fn close(&self) -> Amount {
self.close
}
}
#[cfg(test)]
mod trade_aggregation_tests {
use super::*;
use serde_json;
fn trade_aggregation_json() -> &'static str {
include_str!("../../fixtures/trade_aggregation.json")
}
#[test]
fn it_parses_into_a_trade() {
let trade_agg: TradeAggregation = serde_json::from_str(&trade_aggregation_json()).unwrap();
assert_eq!(
trade_agg.started_at(),
Utc.ymd(2018, 2, 1).and_hms(22, 0, 0)
);
assert_eq!(trade_agg.count(), 26);
assert_eq!(trade_agg.base_volume(), Amount::new(275750201596));
assert_eq!(trade_agg.counter_volume(), Amount::new(50856410385));
assert_eq!(trade_agg.average(), Amount::new(1844293));
assert_eq!(trade_agg.high(), Amount::new(1915709));
assert_eq!(trade_agg.low(), Amount::new(1506024));
assert_eq!(trade_agg.open(), Amount::new(1724138));
assert_eq!(trade_agg.close(), Amount::new(1506024));
}
}