use serde::{Deserialize, Serialize};
use super::common::{PriceLevel, TotalStats, TradeInfo, TradingHalt};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct Quote {
pub date: String,
#[serde(rename = "type")]
pub data_type: Option<String>,
pub exchange: Option<String>,
pub market: Option<String>,
pub symbol: String,
pub name: Option<String>,
#[serde(rename = "previousClose", default)]
pub previous_close: Option<f64>,
#[serde(rename = "openPrice")]
pub open_price: Option<f64>,
#[serde(rename = "openTime")]
pub open_time: Option<i64>,
#[serde(rename = "highPrice")]
pub high_price: Option<f64>,
#[serde(rename = "highTime")]
pub high_time: Option<i64>,
#[serde(rename = "lowPrice")]
pub low_price: Option<f64>,
#[serde(rename = "lowTime")]
pub low_time: Option<i64>,
#[serde(rename = "closePrice")]
pub close_price: Option<f64>,
#[serde(rename = "closeTime")]
pub close_time: Option<i64>,
#[serde(rename = "lastPrice")]
pub last_price: Option<f64>,
#[serde(rename = "lastSize")]
pub last_size: Option<i64>,
#[serde(rename = "avgPrice")]
pub avg_price: Option<f64>,
pub change: Option<f64>,
#[serde(rename = "changePercent")]
pub change_percent: Option<f64>,
pub amplitude: Option<f64>,
#[serde(default)]
pub bids: Vec<PriceLevel>,
#[serde(default)]
pub asks: Vec<PriceLevel>,
pub total: Option<TotalStats>,
#[serde(rename = "lastTrade")]
pub last_trade: Option<TradeInfo>,
#[serde(rename = "lastTrial")]
pub last_trial: Option<TradeInfo>,
#[serde(rename = "tradingHalt")]
pub trading_halt: Option<TradingHalt>,
#[serde(rename = "isLimitDownPrice", default)]
pub is_limit_down_price: bool,
#[serde(rename = "isLimitUpPrice", default)]
pub is_limit_up_price: bool,
#[serde(rename = "isLimitDownBid", default)]
pub is_limit_down_bid: bool,
#[serde(rename = "isLimitUpBid", default)]
pub is_limit_up_bid: bool,
#[serde(rename = "isLimitDownAsk", default)]
pub is_limit_down_ask: bool,
#[serde(rename = "isLimitUpAsk", default)]
pub is_limit_up_ask: bool,
#[serde(rename = "isLimitDownHalt", default)]
pub is_limit_down_halt: bool,
#[serde(rename = "isLimitUpHalt", default)]
pub is_limit_up_halt: bool,
#[serde(rename = "isTrial", default)]
pub is_trial: bool,
#[serde(rename = "isDelayedOpen", default)]
pub is_delayed_open: bool,
#[serde(rename = "isDelayedClose", default)]
pub is_delayed_close: bool,
#[serde(rename = "isContinuous", default)]
pub is_continuous: bool,
#[serde(rename = "isOpen", default)]
pub is_open: bool,
#[serde(rename = "isClose", default)]
pub is_close: bool,
#[serde(rename = "lastUpdated")]
pub last_updated: Option<i64>,
}
impl Quote {
pub fn spread(&self) -> Option<f64> {
let best_bid = self.bids.first().map(|l| l.price);
let best_ask = self.asks.first().map(|l| l.price);
match (best_ask, best_bid) {
(Some(ask), Some(bid)) => Some(ask - bid),
_ => None,
}
}
pub fn mid_price(&self) -> Option<f64> {
let best_bid = self.bids.first().map(|l| l.price);
let best_ask = self.asks.first().map(|l| l.price);
match (best_ask, best_bid) {
(Some(ask), Some(bid)) => Some((ask + bid) / 2.0),
_ => None,
}
}
pub fn has_price_data(&self) -> bool {
self.last_price.is_some() || self.close_price.is_some()
}
pub fn is_at_limit_up(&self) -> bool {
self.is_limit_up_price
}
pub fn is_at_limit_down(&self) -> bool {
self.is_limit_down_price
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quote_deserialization() {
let json = r#"{
"date": "2024-01-15",
"type": "EQUITY",
"exchange": "TWSE",
"market": "TSE",
"symbol": "2330",
"name": "台積電",
"previousClose": 580.0,
"openPrice": 580.0,
"openTime": 1705287000000,
"highPrice": 585.0,
"highTime": 1705290600000,
"lowPrice": 578.0,
"lowTime": 1705288800000,
"closePrice": 583.0,
"closeTime": 1705302000000,
"lastPrice": 583.0,
"lastSize": 1000,
"avgPrice": 581.5,
"change": 3.0,
"changePercent": 0.52,
"amplitude": 1.21,
"bids": [
{"price": 582.0, "size": 500},
{"price": 581.0, "size": 300}
],
"asks": [
{"price": 583.0, "size": 200},
{"price": 584.0, "size": 400}
],
"total": {
"tradeValue": 5815000000,
"tradeVolume": 10000000,
"transaction": 50000
},
"isLimitUpPrice": false,
"isLimitDownPrice": false,
"isTrial": false,
"isOpen": true,
"isClose": false,
"lastUpdated": 1705302000000
}"#;
let quote: Quote = serde_json::from_str(json).unwrap();
assert_eq!(quote.symbol, "2330");
assert_eq!(quote.name.as_deref(), Some("台積電"));
assert_eq!(quote.previous_close, Some(580.0));
assert_eq!(quote.last_price, Some(583.0));
assert_eq!(quote.bids.len(), 2);
assert_eq!(quote.asks.len(), 2);
assert_eq!(quote.bids[0].price, 582.0);
assert_eq!(quote.asks[0].price, 583.0);
}
#[test]
fn test_quote_spread() {
let quote = Quote {
bids: vec![PriceLevel { price: 100.0, size: 100 }],
asks: vec![PriceLevel { price: 100.5, size: 100 }],
..Default::default()
};
assert_eq!(quote.spread(), Some(0.5));
}
#[test]
fn test_quote_mid_price() {
let quote = Quote {
bids: vec![PriceLevel { price: 100.0, size: 100 }],
asks: vec![PriceLevel { price: 101.0, size: 100 }],
..Default::default()
};
assert_eq!(quote.mid_price(), Some(100.5));
}
#[test]
fn test_quote_minimal() {
let json = r#"{"date": "2024-01-15", "symbol": "2330"}"#;
let quote: Quote = serde_json::from_str(json).unwrap();
assert_eq!(quote.symbol, "2330");
assert!(quote.bids.is_empty());
assert!(quote.asks.is_empty());
}
#[test]
fn test_quote_limit_flags() {
let json = r#"{
"date": "2024-01-15",
"symbol": "2330",
"isLimitUpPrice": true,
"isLimitDownPrice": false
}"#;
let quote: Quote = serde_json::from_str(json).unwrap();
assert!(quote.is_at_limit_up());
assert!(!quote.is_at_limit_down());
}
}