use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use super::types::{AspNetDate, FlexBool};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Trade {
pub date: Option<AspNetDate>,
pub start_date: Option<AspNetDate>,
pub end_date: Option<AspNetDate>,
#[serde(rename = "TD30")]
pub td_30: Option<AspNetDate>,
#[serde(rename = "TD90")]
pub td_90: Option<AspNetDate>,
#[serde(rename = "TD1CY")]
pub td_1cy: Option<AspNetDate>,
pub date_key: Option<i64>,
pub time_key: Option<i64>,
pub security_key: Option<i64>,
#[serde(rename = "TradeID")]
pub trade_id: Option<i64>,
pub sequence_number: Option<i64>,
#[serde(rename = "EOM")]
pub eom: Option<FlexBool>,
#[serde(rename = "EOQ")]
pub eoq: Option<FlexBool>,
#[serde(rename = "EOY")]
pub eoy: Option<FlexBool>,
#[serde(rename = "OPEX")]
pub opex: Option<FlexBool>,
#[serde(rename = "VOLEX")]
pub volex: Option<FlexBool>,
pub ticker: Option<String>,
pub sector: Option<String>,
pub industry: Option<String>,
pub name: Option<String>,
pub full_date_time: Option<String>,
#[serde(rename = "FullTimeString24")]
pub full_time_string_24: Option<String>,
pub price: Option<Decimal>,
pub bid: Option<Decimal>,
pub ask: Option<Decimal>,
pub dollars: Option<Decimal>,
pub average_block_size_dollars: Option<Decimal>,
pub average_block_size_shares: Option<i64>,
pub dollars_multiplier: Option<f64>,
pub volume: Option<i64>,
pub average_daily_volume: Option<i64>,
pub percent_daily_volume: Option<f64>,
pub relative_size: Option<f64>,
pub last_comparible_trade_date: Option<AspNetDate>,
#[serde(rename = "IPODate")]
pub ipo_date: Option<AspNetDate>,
pub offsetting_trade_date: Option<AspNetDate>,
pub phantom_print_fulfillment_date: Option<AspNetDate>,
pub phantom_print_fulfillment_days: Option<i64>,
pub trade_count: Option<i64>,
pub cumulative_distribution: Option<f64>,
pub trade_rank: Option<i64>,
pub trade_rank_snapshot: Option<i64>,
pub late_print: Option<FlexBool>,
pub sweep: Option<FlexBool>,
pub dark_pool: Option<FlexBool>,
pub opening_trade: Option<FlexBool>,
pub closing_trade: Option<FlexBool>,
pub phantom_print: Option<FlexBool>,
pub inside_bar: Option<FlexBool>,
pub double_inside_bar: Option<FlexBool>,
pub signature_print: Option<FlexBool>,
pub new_position: Option<FlexBool>,
#[serde(rename = "AHInstitutionalDollars")]
pub ah_institutional_dollars: Option<Decimal>,
#[serde(rename = "AHInstitutionalDollarsRank")]
pub ah_institutional_dollars_rank: Option<i64>,
#[serde(rename = "AHInstitutionalVolume")]
pub ah_institutional_volume: Option<i64>,
pub total_institutional_dollars: Option<Decimal>,
pub total_institutional_dollars_rank: Option<i64>,
pub total_institutional_volume: Option<i64>,
pub closing_trade_dollars: Option<Decimal>,
pub closing_trade_dollars_rank: Option<i64>,
pub closing_trade_volume: Option<i64>,
pub total_dollars: Option<Decimal>,
pub total_dollars_rank: Option<i64>,
pub total_volume: Option<i64>,
pub close_price: Option<Decimal>,
#[serde(rename = "RSIHour")]
pub rsi_hour: Option<f64>,
#[serde(rename = "RSIDay")]
pub rsi_day: Option<f64>,
pub total_rows: Option<i64>,
pub trade_conditions: Option<String>,
#[serde(rename = "FrequencyLast30TD")]
pub frequency_last_30_td: Option<i64>,
#[serde(rename = "FrequencyLast90TD")]
pub frequency_last_90_td: Option<i64>,
#[serde(rename = "FrequencyLast1CY")]
pub frequency_last_1cy: Option<i64>,
pub cancelled: Option<FlexBool>,
pub total_trades: Option<i64>,
pub external_feed: Option<FlexBool>,
}
#[cfg(test)]
mod tests {
use super::*;
fn dec(v: f64) -> Decimal {
Decimal::try_from(v).unwrap()
}
#[derive(Deserialize)]
struct TradesResponse {
data: Vec<Trade>,
}
#[test]
fn deserialize_trades_from_fixture() {
let json = include_str!("../../tests/fixtures/trades_get_trades_response.json");
let response: TradesResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.data.len(), 2);
let first = &response.data[0];
assert_eq!(first.ticker.as_deref(), Some("AXP"));
assert_eq!(first.sector.as_deref(), Some("Financial Services"));
assert_eq!(first.industry.as_deref(), Some("Consumer Finance"));
assert_eq!(first.trade_id, Some(71_774_613_157_188));
assert_eq!(first.price, Some(dec(319.68)));
assert_eq!(first.volume, Some(276_248));
assert_eq!(first.dark_pool, Some(FlexBool(Some(true))));
assert_eq!(first.late_print, Some(FlexBool(Some(false))));
assert_eq!(first.eom, Some(FlexBool(Some(false))));
let td30 = first.td_30.as_ref().unwrap();
assert!(td30.0.is_none());
let date = first.date.as_ref().unwrap();
assert!(date.0.is_some());
assert!(first.phantom_print_fulfillment_days.is_none());
assert!(first.trade_conditions.is_none());
}
#[test]
fn deserialize_trade_empty_phantom_date() {
let json = include_str!("../../tests/fixtures/trades_get_trades_response.json");
let response: TradesResponse = serde_json::from_str(json).unwrap();
let second = &response.data[1];
let phantom = second.phantom_print_fulfillment_date.as_ref().unwrap();
assert!(phantom.0.is_none());
assert_eq!(second.ticker.as_deref(), Some("MRVL"));
assert_eq!(second.eom, Some(FlexBool(Some(true))));
}
#[test]
fn deserialize_trade_nullable_strings_null() {
let json = r#"{
"Industry": null,
"FullDateTime": null,
"FullTimeString24": null
}"#;
let trade: Trade = serde_json::from_str(json).unwrap();
assert!(trade.industry.is_none());
assert!(trade.full_date_time.is_none());
assert!(trade.full_time_string_24.is_none());
}
#[test]
fn deserialize_trade_nullable_strings_present() {
let json = r#"{
"Industry": "Consumer Finance",
"FullDateTime": "2026-05-01T16:20:51",
"FullTimeString24": "16:20:51"
}"#;
let trade: Trade = serde_json::from_str(json).unwrap();
assert_eq!(trade.industry.as_deref(), Some("Consumer Finance"));
assert_eq!(trade.full_date_time.as_deref(), Some("2026-05-01T16:20:51"));
assert_eq!(trade.full_time_string_24.as_deref(), Some("16:20:51"));
}
}