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 VolumeAtPrice {
pub price: f64,
pub volume: i64,
#[serde(rename = "volumeAtBid")]
pub volume_at_bid: Option<i64>,
#[serde(rename = "volumeAtAsk")]
pub volume_at_ask: Option<i64>,
}
impl VolumeAtPrice {
pub fn imbalance(&self) -> Option<i64> {
match (self.volume_at_ask, self.volume_at_bid) {
(Some(ask), Some(bid)) => Some(ask - bid),
_ => None,
}
}
pub fn buy_ratio(&self) -> Option<f64> {
match (self.volume_at_ask, self.volume_at_bid) {
(Some(ask), Some(bid)) if ask + bid > 0 => {
Some(ask as f64 / (ask + bid) as f64)
}
_ => None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct VolumesResponse {
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<VolumeAtPrice>,
}
impl VolumesResponse {
pub fn total_volume(&self) -> i64 {
self.data.iter().map(|v| v.volume).sum()
}
pub fn total_volume_at_bid(&self) -> i64 {
self.data.iter().filter_map(|v| v.volume_at_bid).sum()
}
pub fn total_volume_at_ask(&self) -> i64 {
self.data.iter().filter_map(|v| v.volume_at_ask).sum()
}
pub fn price_with_max_volume(&self) -> Option<f64> {
self.data
.iter()
.max_by_key(|v| v.volume)
.map(|v| v.price)
}
pub fn vwap(&self) -> Option<f64> {
let total_vol = self.total_volume();
if total_vol == 0 {
return None;
}
let total_value: f64 = self.data
.iter()
.map(|v| v.price * v.volume as f64)
.sum();
Some(total_value / total_vol as f64)
}
pub fn net_imbalance(&self) -> i64 {
self.total_volume_at_ask() - self.total_volume_at_bid()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_volume_at_price_deserialization() {
let json = r#"{
"price": 583.0,
"volume": 10000,
"volumeAtBid": 4000,
"volumeAtAsk": 6000
}"#;
let vol: VolumeAtPrice = serde_json::from_str(json).unwrap();
assert_eq!(vol.price, 583.0);
assert_eq!(vol.volume, 10000);
assert_eq!(vol.imbalance(), Some(2000)); assert_eq!(vol.buy_ratio(), Some(0.6)); }
#[test]
fn test_volumes_response_deserialization() {
let json = r#"{
"date": "2024-01-15",
"type": "EQUITY",
"exchange": "TWSE",
"market": "TSE",
"symbol": "2330",
"data": [
{"price": 580.0, "volume": 5000, "volumeAtBid": 3000, "volumeAtAsk": 2000},
{"price": 583.0, "volume": 10000, "volumeAtBid": 4000, "volumeAtAsk": 6000},
{"price": 585.0, "volume": 3000, "volumeAtBid": 1000, "volumeAtAsk": 2000}
]
}"#;
let response: VolumesResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.symbol, "2330");
assert_eq!(response.data.len(), 3);
assert_eq!(response.total_volume(), 18000);
assert_eq!(response.total_volume_at_bid(), 8000);
assert_eq!(response.total_volume_at_ask(), 10000);
assert_eq!(response.price_with_max_volume(), Some(583.0));
assert_eq!(response.net_imbalance(), 2000); }
#[test]
fn test_volumes_vwap() {
let response = VolumesResponse {
data: vec![
VolumeAtPrice { price: 100.0, volume: 1000, ..Default::default() },
VolumeAtPrice { price: 101.0, volume: 1000, ..Default::default() },
],
..Default::default()
};
assert_eq!(response.vwap(), Some(100.5));
}
}