use std::time::SystemTime;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::{TimestampSeconds, serde_as};
use crate::types::core::Side;
use crate::types::js_safe_ints::JsSafeU64;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LiquidityRole {
Maker,
Taker,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TradeType {
Limit,
Market,
Liquidation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradesMessage {
pub symbol: String,
pub trades: Vec<TradeEvent>,
}
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradeEvent {
pub slot: JsSafeU64,
pub slot_index: u32,
#[serde_as(as = "TimestampSeconds<String>")]
pub timestamp: SystemTime,
pub symbol: String,
pub taker: String,
pub trade_sequence_number: JsSafeU64,
pub side: Side,
pub base_lots_filled: JsSafeU64,
pub quote_lots_filled: JsSafeU64,
pub fee_in_quote_lots: JsSafeU64,
pub base_amount: f64,
pub quote_amount: f64,
pub num_fills: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct TradesSubscriptionRequest {
pub symbol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradeHistoryItem {
pub user_id: i64,
pub trader_id: i64,
pub trader_pda_index: i32,
pub subaccount_index: i32,
pub market_symbol: String,
pub signature: Option<String>,
#[serde(default)]
pub fill_id: Option<String>,
pub timestamp: DateTime<Utc>,
pub slot: i64,
pub slot_index: i32,
pub event_index: i32,
pub instruction_index: i32,
pub instruction_type: String,
pub base_lots_before: String,
pub base_lots_after: String,
pub base_lots_delta: String,
pub virtual_quote_lots_before: String,
pub virtual_quote_lots_after: String,
pub virtual_quote_lots_delta: String,
pub price: String,
pub realized_pnl: String,
pub fees: String,
pub liquidity: LiquidityRole,
pub order_sequence_number: Option<i64>,
pub spline_sequence_number: Option<i64>,
pub trade_type: TradeType,
}
pub type TradeHistoryResponse = crate::types::core::PaginatedResponse<Vec<TradeHistoryItem>>;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradeHistoryQueryParams {
#[serde(default, alias = "pdaIndex", alias = "pda_index")]
pub pda_index: u8,
#[serde(skip_serializing_if = "Option::is_none")]
pub market_symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
}
impl TradeHistoryQueryParams {
pub fn new() -> Self {
Self::default()
}
pub fn with_pda_index(mut self, pda_index: u8) -> Self {
self.pda_index = pda_index;
self
}
pub fn with_market_symbol(mut self, symbol: impl Into<String>) -> Self {
self.market_symbol = Some(symbol.into());
self
}
pub fn with_limit(mut self, limit: i64) -> Self {
self.limit = Some(limit);
self
}
pub fn with_cursor(mut self, cursor: impl Into<String>) -> Self {
self.cursor = Some(cursor.into());
self
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_deserialize_trade_event() {
let json = r#"{
"slot": "123456789",
"slotIndex": 5,
"timestamp": "1775578550",
"symbol": "SOL",
"taker": "ABC123pubkey",
"tradeSequenceNumber": "100",
"side": "bid",
"baseLotsFilled": "1000",
"quoteLotsFilled": "150000",
"feeInQuoteLots": "30",
"baseAmount": 10.0,
"quoteAmount": 1500.0,
"numFills": 2
}"#;
let trade: TradeEvent = serde_json::from_str(json).unwrap();
assert_eq!(trade.symbol, "SOL");
assert_eq!(trade.base_amount, 10.0);
assert_eq!(trade.quote_amount, 1500.0);
assert_eq!(trade.num_fills, 2);
assert_eq!(trade.side, Side::Bid);
assert_eq!(
trade.timestamp,
SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1775578550)
);
}
#[test]
fn test_serialize_trade_event() {
let trade = TradeEvent {
slot: 123456789u64.into(),
slot_index: 5,
timestamp: SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1775578550),
symbol: "SOL".to_string(),
taker: "ABC123pubkey".to_string(),
trade_sequence_number: 100u64.into(),
side: Side::Ask,
base_lots_filled: 1000u64.into(),
quote_lots_filled: 150000u64.into(),
fee_in_quote_lots: 30u64.into(),
base_amount: 10.0,
quote_amount: 1500.0,
num_fills: 2,
};
let json = serde_json::to_string(&trade).unwrap();
assert!(json.contains("\"symbol\":\"SOL\""));
assert!(json.contains("\"baseAmount\":10"));
assert!(json.contains("\"side\":\"ask\""));
}
#[test]
fn test_trades_subscription_request() {
let req = TradesSubscriptionRequest {
symbol: "SOL".to_string(),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains("\"symbol\":\"SOL\""));
}
#[test]
fn test_deserialize_trade_history_item_with_fill_id() {
let json = json!({
"userId": 1,
"traderId": 2,
"traderPdaIndex": 0,
"subaccountIndex": 0,
"marketSymbol": "SOL-PERP",
"signature": "5PRu7zP_mnhT5c",
"fillId": "43148c7f-1389-34f5-99f0-c7296f5858c2",
"timestamp": "2026-04-21T12:00:00Z",
"slot": 377700000,
"slotIndex": 2442,
"eventIndex": 0,
"instructionIndex": 4,
"instructionType": "PlaceMarketOrder",
"baseLotsBefore": "0",
"baseLotsAfter": "1",
"baseLotsDelta": "1",
"virtualQuoteLotsBefore": "0",
"virtualQuoteLotsAfter": "150000",
"virtualQuoteLotsDelta": "150000",
"price": "150",
"realizedPnl": "1",
"fees": "0.1",
"liquidity": "taker",
"orderSequenceNumber": null,
"splineSequenceNumber": null,
"tradeType": "market"
});
let item: TradeHistoryItem = serde_json::from_value(json).unwrap();
assert_eq!(
item.fill_id.as_deref(),
Some("43148c7f-1389-34f5-99f0-c7296f5858c2")
);
}
#[test]
fn test_deserialize_trade_history_item_without_fill_id() {
let json = json!({
"userId": 1,
"traderId": 2,
"traderPdaIndex": 0,
"subaccountIndex": 0,
"marketSymbol": "SOL-PERP",
"signature": "5PRu7zP_mnhT5c",
"timestamp": "2026-04-21T12:00:00Z",
"slot": 377700000,
"slotIndex": 2442,
"eventIndex": 0,
"instructionIndex": 4,
"instructionType": "PlaceMarketOrder",
"baseLotsBefore": "0",
"baseLotsAfter": "1",
"baseLotsDelta": "1",
"virtualQuoteLotsBefore": "0",
"virtualQuoteLotsAfter": "150000",
"virtualQuoteLotsDelta": "150000",
"price": "150",
"realizedPnl": "1",
"fees": "0.1",
"liquidity": "taker",
"orderSequenceNumber": null,
"splineSequenceNumber": null,
"tradeType": "market"
});
let item: TradeHistoryItem = serde_json::from_value(json).unwrap();
assert_eq!(item.fill_id, None);
}
}