use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct Trade {
pub bid: Option<f64>,
pub ask: Option<f64>,
pub price: f64,
pub size: i64,
pub time: i64,
#[serde(default)]
pub serial: Option<i64>,
#[serde(default)]
pub volume: Option<i64>,
}
impl Trade {
pub fn infer_direction(&self) -> &'static str {
match (self.bid, self.ask) {
(Some(bid), _) if (self.price - bid).abs() < 0.0001 => "S",
(_, Some(ask)) if (self.price - ask).abs() < 0.0001 => "B",
_ => "N",
}
}
pub fn is_buyer_initiated(&self) -> bool {
self.infer_direction() == "B"
}
pub fn is_seller_initiated(&self) -> bool {
self.infer_direction() == "S"
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct TradesResponse {
pub date: String,
#[serde(rename = "type")]
pub data_type: Option<String>,
pub exchange: Option<String>,
pub market: Option<String>,
pub symbol: String,
#[serde(default)]
pub data: Vec<Trade>,
}
impl TradesResponse {
pub fn total_volume(&self) -> i64 {
self.data.iter().map(|t| t.size).sum()
}
pub fn total_value(&self) -> f64 {
self.data.iter().map(|t| t.price * t.size as f64).sum()
}
pub fn vwap(&self) -> Option<f64> {
let total_volume = self.total_volume();
if total_volume == 0 {
return None;
}
Some(self.total_value() / total_volume as f64)
}
pub fn trades_in_range(&self, start: i64, end: i64) -> Vec<&Trade> {
self.data
.iter()
.filter(|t| t.time >= start && t.time <= end)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trade_deserialization() {
let json = r#"{
"bid": 582.0,
"ask": 583.0,
"price": 583.0,
"size": 1000,
"time": 1705287000000
}"#;
let trade: Trade = serde_json::from_str(json).unwrap();
assert_eq!(trade.price, 583.0);
assert_eq!(trade.size, 1000);
assert!(trade.is_buyer_initiated());
}
#[test]
fn test_trade_direction() {
let buy_trade = Trade {
bid: Some(100.0),
ask: Some(100.5),
price: 100.5,
size: 100,
time: 0,
..Default::default()
};
assert_eq!(buy_trade.infer_direction(), "B");
let sell_trade = Trade {
bid: Some(100.0),
ask: Some(100.5),
price: 100.0,
size: 100,
time: 0,
..Default::default()
};
assert_eq!(sell_trade.infer_direction(), "S");
let neutral_trade = Trade {
bid: Some(100.0),
ask: Some(101.0),
price: 100.5,
size: 100,
time: 0,
..Default::default()
};
assert_eq!(neutral_trade.infer_direction(), "N");
}
#[test]
fn test_trades_response_deserialization() {
let json = r#"{
"date": "2024-01-15",
"type": "EQUITY",
"exchange": "TWSE",
"market": "TSE",
"symbol": "2330",
"data": [
{"bid": 582.0, "ask": 583.0, "price": 583.0, "size": 1000, "time": 1705287000000},
{"bid": 582.0, "ask": 583.0, "price": 582.0, "size": 500, "time": 1705287001000}
]
}"#;
let response: TradesResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.symbol, "2330");
assert_eq!(response.data.len(), 2);
assert_eq!(response.total_volume(), 1500);
}
#[test]
fn test_trades_vwap() {
let response = TradesResponse {
data: vec![
Trade { price: 100.0, size: 1000, ..Default::default() },
Trade { price: 101.0, size: 1000, ..Default::default() },
],
..Default::default()
};
assert_eq!(response.vwap(), Some(100.5));
}
#[test]
fn test_trades_empty() {
let response = TradesResponse::default();
assert_eq!(response.total_volume(), 0);
assert_eq!(response.vwap(), None);
}
}