use crate::fixed_point::FixedPoint;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub enum DataSource {
BinanceSpot,
#[default]
BinanceFuturesUM,
BinanceFuturesCM,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub enum BreachMode {
#[default]
Last,
Portcullis,
Mid,
Directional,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
pub struct Tick {
#[serde(alias = "agg_trade_id")]
pub ref_id: i64,
pub price: FixedPoint,
pub volume: FixedPoint,
#[serde(alias = "first_trade_id")]
pub first_sub_id: i64,
#[serde(alias = "last_trade_id")]
pub last_sub_id: i64,
pub timestamp: i64,
pub is_buyer_maker: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_best_match: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub best_bid: Option<FixedPoint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub best_ask: Option<FixedPoint>,
}
impl Tick {
#[inline]
pub fn individual_trade_count(&self) -> i64 {
self.last_sub_id - self.first_sub_id + 1
}
#[inline]
pub fn turnover(&self) -> i128 {
(self.price.0 as i128) * (self.volume.0 as i128)
}
}
pub type AggTrade = Tick;
#[cfg(test)]
mod tests {
use super::*;
fn make_trade(price: &str, volume: &str, first_id: i64, last_id: i64) -> Tick {
Tick {
ref_id: 1,
price: FixedPoint::from_str(price).unwrap(),
volume: FixedPoint::from_str(volume).unwrap(),
first_sub_id: first_id,
last_sub_id: last_id,
timestamp: 1000,
is_buyer_maker: false,
is_best_match: None,
best_bid: None,
best_ask: None,
}
}
#[test]
fn test_tick_is_copy() {
let a = make_trade("100.0", "1.0", 5, 5);
let b = a; let c = a; assert_eq!(b.ref_id, c.ref_id);
assert_eq!(b.price.0, c.price.0);
}
#[test]
fn test_individual_trade_count_single() {
let trade = make_trade("100.0", "1.0", 5, 5);
assert_eq!(trade.individual_trade_count(), 1);
}
#[test]
fn test_individual_trade_count_multiple() {
let trade = make_trade("100.0", "1.0", 100, 199);
assert_eq!(trade.individual_trade_count(), 100);
}
#[test]
fn test_individual_trade_count_large_range() {
let trade = make_trade("100.0", "1.0", 0, 999_999);
assert_eq!(trade.individual_trade_count(), 1_000_000);
}
#[test]
fn test_turnover_basic() {
let trade = make_trade("100.0", "2.0", 1, 1);
let expected = 10_000_000_000i128 * 200_000_000i128;
assert_eq!(trade.turnover(), expected);
}
#[test]
fn test_turnover_zero_volume() {
let trade = make_trade("100.0", "0.0", 1, 1);
assert_eq!(trade.turnover(), 0);
}
#[test]
fn test_turnover_large_values_no_overflow() {
let trade = make_trade("0.00002", "10000000000.0", 1, 1);
let turnover = trade.turnover();
assert!(turnover > 0, "Turnover should be positive for valid trade");
let _as_f64 = turnover as f64;
}
#[test]
fn test_turnover_tiny_price() {
let trade = make_trade("0.00000001", "1.0", 1, 1);
assert_eq!(trade.turnover(), 100_000_000);
}
#[test]
fn test_serde_backward_compat() {
let json = r#"{
"agg_trade_id": 42,
"price": 5000000000000,
"volume": 100000000,
"first_trade_id": 100,
"last_trade_id": 105,
"timestamp": 1000000,
"is_buyer_maker": false
}"#;
let tick: Tick = serde_json::from_str(json).unwrap();
assert_eq!(tick.ref_id, 42);
assert_eq!(tick.first_sub_id, 100);
assert_eq!(tick.last_sub_id, 105);
assert_eq!(tick.best_bid, None);
assert_eq!(tick.best_ask, None);
}
}