use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TradeRecord {
pub time: i64, pub side: String, pub price: f64,
pub size: f64,
pub symbol: String,
pub is_paper: bool,
pub agent_id: String,
}
pub fn parse_timestamp_to_unix(ts: &str) -> Option<i64> {
if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(ts) {
return Some(dt.timestamp());
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(ts, "%Y-%m-%dT%H:%M:%S") {
return Some(dt.and_utc().timestamp());
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(ts, "%Y-%m-%dT%H:%M:%S%.f") {
return Some(dt.and_utc().timestamp());
}
None
}
pub fn action_to_side(action: &str) -> Option<String> {
let lower = action.to_lowercase();
if lower == "buy" || lower == "long" {
Some("buy".to_string())
} else if lower == "sell" || lower == "short" {
Some("sell".to_string())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_timestamp_rfc3339() {
let ts = "2026-03-09T14:32:00Z";
let unix = parse_timestamp_to_unix(ts).unwrap();
assert!(unix > 0);
}
#[test]
fn test_parse_timestamp_with_offset() {
let ts = "2026-03-09T14:32:00+08:00";
let unix = parse_timestamp_to_unix(ts).unwrap();
assert!(unix > 0);
}
#[test]
fn test_parse_timestamp_naive() {
let ts = "2026-03-09T14:32:00";
let unix = parse_timestamp_to_unix(ts).unwrap();
assert!(unix > 0);
}
#[test]
fn test_parse_timestamp_invalid() {
assert!(parse_timestamp_to_unix("not-a-date").is_none());
}
#[test]
fn test_action_to_side_buy() {
assert_eq!(action_to_side("buy"), Some("buy".to_string()));
assert_eq!(action_to_side("Buy"), Some("buy".to_string()));
assert_eq!(action_to_side("long"), Some("buy".to_string()));
}
#[test]
fn test_action_to_side_sell() {
assert_eq!(action_to_side("sell"), Some("sell".to_string()));
assert_eq!(action_to_side("Sell"), Some("sell".to_string()));
assert_eq!(action_to_side("short"), Some("sell".to_string()));
}
#[test]
fn test_action_to_side_none() {
assert_eq!(action_to_side("hold"), None);
assert_eq!(action_to_side(""), None);
}
#[test]
fn test_trade_record_serialization() {
let record = TradeRecord {
time: 1741520000,
side: "buy".to_string(),
price: 95000.0,
size: 0.05,
symbol: "BTC-PERP".to_string(),
is_paper: false,
agent_id: "agent-1".to_string(),
};
let json = serde_json::to_value(&record).unwrap();
assert_eq!(json["time"], 1741520000);
assert_eq!(json["side"], "buy");
assert_eq!(json["price"], 95000.0);
assert_eq!(json["size"], 0.05);
assert_eq!(json["symbol"], "BTC-PERP");
assert_eq!(json["isPaper"], false);
assert_eq!(json["agentId"], "agent-1");
}
#[test]
fn test_trade_record_deserialization() {
let json = serde_json::json!({
"time": 1741520000,
"side": "sell",
"price": 96000.0,
"size": 0.1,
"symbol": "ETH-PERP",
"isPaper": true,
"agentId": "agent-2"
});
let record: TradeRecord = serde_json::from_value(json).unwrap();
assert_eq!(record.time, 1741520000);
assert_eq!(record.side, "sell");
assert_eq!(record.price, 96000.0);
assert_eq!(record.size, 0.1);
assert_eq!(record.symbol, "ETH-PERP");
assert!(record.is_paper);
assert_eq!(record.agent_id, "agent-2");
}
}