1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct PublicTrade {
6 pub proxy_wallet: String,
7 pub side: String,
8 pub asset: String,
9 pub condition_id: String,
10 pub size: f64,
11 pub price: f64,
12 pub timestamp: DateTime<Utc>,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
14 pub title: Option<String>,
15 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub slug: Option<String>,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 pub icon: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub event_slug: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub outcome: Option<String>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub outcome_index: Option<u32>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub name: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub pseudonym: Option<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub bio: Option<String>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub profile_image: Option<String>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub profile_image_optimized: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub transaction_hash: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45pub struct MarketTrade {
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub id: Option<String>,
48 pub price: f64,
49 pub size: f64,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub side: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub aggressor_side: Option<String>,
54 pub timestamp: DateTime<Utc>,
55 pub source_channel: std::borrow::Cow<'static, str>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub tx_hash: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub outcome: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub yes_price: Option<f64>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub no_price: Option<f64>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub taker_address: Option<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PricePoint {
70 pub timestamp: DateTime<Utc>,
71 pub price: f64,
72 #[serde(default)]
73 pub raw: serde_json::Value,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81pub struct Candlestick {
82 pub timestamp: DateTime<Utc>,
84 pub open: f64,
85 pub high: f64,
86 pub low: f64,
87 pub close: f64,
88 pub volume: f64,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub open_interest: Option<f64>,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
97pub enum PriceHistoryInterval {
98 #[serde(rename = "1m")]
99 OneMinute,
100 #[serde(rename = "1h")]
101 OneHour,
102 #[serde(rename = "6h")]
103 SixHours,
104 #[serde(rename = "1d")]
105 OneDay,
106 #[serde(rename = "1w")]
107 OneWeek,
108 #[serde(rename = "max")]
109 Max,
110}
111
112impl PriceHistoryInterval {
113 pub fn as_str(&self) -> &'static str {
114 match self {
115 Self::OneMinute => "1m",
116 Self::OneHour => "1h",
117 Self::SixHours => "6h",
118 Self::OneDay => "1d",
119 Self::OneWeek => "1w",
120 Self::Max => "max",
121 }
122 }
123
124 pub fn seconds(&self) -> i64 {
126 match self {
127 Self::OneMinute => 60,
128 Self::OneHour => 3600,
129 Self::SixHours => 21600,
130 Self::OneDay => 86400,
131 Self::OneWeek => 604_800,
132 Self::Max => 86400,
133 }
134 }
135}
136
137impl std::str::FromStr for PriceHistoryInterval {
138 type Err = String;
139
140 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 match s {
142 "1m" => Ok(Self::OneMinute),
143 "1h" => Ok(Self::OneHour),
144 "6h" => Ok(Self::SixHours),
145 "1d" => Ok(Self::OneDay),
146 "1w" => Ok(Self::OneWeek),
147 "max" => Ok(Self::Max),
148 _ => Err(format!("unknown interval: {s}")),
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn candlestick_omits_open_interest_when_none() {
159 let c = Candlestick {
160 timestamp: chrono::Utc::now(),
161 open: 0.5,
162 high: 0.6,
163 low: 0.4,
164 close: 0.55,
165 volume: 100.0,
166 open_interest: None,
167 };
168 let json = serde_json::to_value(&c).unwrap();
169 assert!(json.get("open_interest").is_none());
170 }
171
172 #[test]
173 fn candlestick_includes_open_interest_when_some() {
174 let c = Candlestick {
175 timestamp: chrono::Utc::now(),
176 open: 0.5,
177 high: 0.6,
178 low: 0.4,
179 close: 0.55,
180 volume: 100.0,
181 open_interest: Some(42000.0),
182 };
183 let json = serde_json::to_value(&c).unwrap();
184 assert_eq!(json["open_interest"], 42000.0);
185 }
186
187 #[test]
188 fn candlestick_roundtrip_with_open_interest() {
189 let c = Candlestick {
190 timestamp: chrono::Utc::now(),
191 open: 0.5,
192 high: 0.6,
193 low: 0.4,
194 close: 0.55,
195 volume: 100.0,
196 open_interest: Some(1234.0),
197 };
198 let serialized = serde_json::to_string(&c).unwrap();
199 let deserialized: Candlestick = serde_json::from_str(&serialized).unwrap();
200 assert_eq!(deserialized.open_interest, Some(1234.0));
201 }
202
203 #[test]
204 fn candlestick_deserialize_without_open_interest_defaults_none() {
205 let json = r#"{"timestamp":"2026-01-01T00:00:00Z","open":0.5,"high":0.6,"low":0.4,"close":0.55,"volume":100.0}"#;
207 let c: Candlestick = serde_json::from_str(json).unwrap();
208 assert_eq!(c.open_interest, None);
209 }
210}