1use chrono::{DateTime, Utc};
4use hypercall_types::{
5 api_models::{FillApiResponse, Order, TradeApiResponse},
6 to_human_readable_decimal, WalletAddress,
7};
8use rust_decimal::Decimal;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TradeRecord {
14 pub trade_id: i64,
15 pub symbol: String,
16 pub price: Decimal,
17 pub size: Decimal,
18 pub maker_address: WalletAddress,
19 pub taker_address: WalletAddress,
20 pub maker_fee: Decimal,
21 pub taker_fee: Decimal,
22 pub timestamp: i64,
23 pub created_at: Option<DateTime<Utc>>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct FillRecord {
29 pub fill_id: Option<i64>,
30 pub trade_id: i64,
31 pub wallet_address: WalletAddress,
32 pub symbol: String,
33 pub price: Decimal,
34 pub size: Decimal,
35 pub fee: Decimal,
36 pub is_taker: bool,
37 pub timestamp: i64,
38 pub builder_code_address: Option<WalletAddress>,
39 pub builder_code_fee: Option<Decimal>,
40 pub realized_pnl: Option<Decimal>,
41 pub underlying_notional: Option<Decimal>,
42 pub created_at: Option<DateTime<Utc>>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct OrderRecord {
48 pub order_id: i64,
49 pub wallet_address: WalletAddress,
50 pub symbol: String,
51 pub side: String,
52 pub price: Decimal,
53 pub size: Decimal,
54 pub filled_size: Decimal,
55 pub status: String,
56 pub tif: String,
57 pub client_id: Option<String>,
58 pub is_perp: bool,
59 pub underlying: Option<String>,
60 pub reduce_only: Option<bool>,
61 pub timestamp: i64,
62 pub created_at: Option<DateTime<Utc>>,
63 pub mmp_enabled: bool,
64}
65
66#[derive(Debug, Clone)]
68pub struct HistoricalPnlPoint {
69 pub timestamp_ms: i64,
70 pub total_equity: Decimal,
71 pub attribution: Option<Vec<u8>>,
72}
73
74#[derive(Debug, Clone)]
76pub struct HistoricalTheoPoint {
77 pub timestamp_ms: i64,
78 pub theoretical_price: Decimal,
79}
80
81#[derive(Debug, Clone)]
83pub struct BboReferenceData {
84 pub reference_ask: Decimal,
85 pub reference_ts: i64,
86 pub used_earliest_fallback: bool,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct VolSurfaceSnapshot {
92 pub id: i64,
93 pub underlying: String,
94 pub interval_ms: i64,
95 pub timestamp_ms: i64,
96 pub surface_json: serde_json::Value,
97 pub created_at: DateTime<Utc>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct BboSnapshotRecord {
103 pub id: i64,
104 pub symbol: String,
105 pub best_bid: Decimal,
106 pub best_ask: Decimal,
107 pub best_bid_size: Option<Decimal>,
108 pub best_ask_size: Option<Decimal>,
109 pub snapshot_ts: i64,
110 pub created_at: DateTime<Utc>,
111}
112
113#[derive(Debug, Clone)]
115pub struct NewBboSnapshotInput {
116 pub symbol: String,
117 pub best_bid: Decimal,
118 pub best_ask: Decimal,
119 pub best_bid_size: Option<Decimal>,
120 pub best_ask_size: Option<Decimal>,
121 pub snapshot_ts: i64,
122}
123
124impl From<TradeRecord> for TradeApiResponse {
125 fn from(r: TradeRecord) -> Self {
126 Self {
127 trade_id: r.trade_id,
128 symbol: r.symbol.clone(),
129 price: r.price,
130 size: to_human_readable_decimal(&r.symbol, r.size),
131 maker_address: r.maker_address,
132 taker_address: r.taker_address,
133 maker_fee: r.maker_fee,
134 taker_fee: r.taker_fee,
135 timestamp: r.timestamp,
136 created_at: r.created_at.unwrap_or_else(Utc::now),
137 }
138 }
139}
140
141impl From<FillRecord> for FillApiResponse {
142 fn from(r: FillRecord) -> Self {
143 Self {
144 fill_id: r.fill_id.unwrap_or(0),
145 trade_id: r.trade_id,
146 wallet_address: r.wallet_address,
147 symbol: r.symbol.clone(),
148 price: r.price,
149 size: to_human_readable_decimal(&r.symbol, r.size),
150 fee: r.fee,
151 is_taker: r.is_taker,
152 timestamp: r.timestamp,
153 created_at: r.created_at.unwrap_or_else(Utc::now),
154 builder_code_address: r.builder_code_address,
155 builder_code_fee: r.builder_code_fee,
156 realized_pnl: r.realized_pnl,
157 explorer_url: None,
158 }
159 }
160}
161
162impl From<OrderRecord> for Order {
163 fn from(r: OrderRecord) -> Self {
164 Self {
165 order_id: r.order_id,
166 wallet_address: r.wallet_address,
167 symbol: r.symbol.clone(),
168 side: r.side,
169 price: r.price,
170 size: to_human_readable_decimal(&r.symbol, r.size),
171 tif: r.tif,
172 status: Some(r.status),
173 created_at: r.timestamp,
174 updated_at: r.created_at,
175 filled_size: Some(to_human_readable_decimal(&r.symbol, r.filled_size)),
176 mmp_enabled: r.mmp_enabled,
177 }
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct LedgerEventTimeDelta {
184 pub event_ts_ms: i64,
185 pub delta: Decimal,
186}
187
188#[derive(Debug, Clone)]
190pub struct FillBackfillRecord {
191 pub symbol: String,
192 pub price: Decimal,
193 pub size: Decimal,
194 pub underlying_notional: Option<Decimal>,
195 pub timestamp: i64,
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use chrono::NaiveDateTime;
202 use hypercall_types::wallet_address::test_wallet;
203 use rust_decimal_macros::dec;
204
205 #[test]
206 fn trade_record_to_api_response() {
207 let ts = NaiveDateTime::from_timestamp_opt(1700000000, 0).unwrap();
208 let created = DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc);
209 let record = TradeRecord {
210 trade_id: 42,
211 symbol: "BTC-20260115-95000-C".into(),
212 price: dec!(100.5),
213 size: dec!(1000000),
214 maker_address: test_wallet(1),
215 taker_address: test_wallet(2),
216 maker_fee: dec!(0.5),
217 taker_fee: dec!(0.3),
218 timestamp: 1700000000000,
219 created_at: Some(created),
220 };
221 let api: TradeApiResponse = record.into();
222 assert_eq!(api.trade_id, 42);
223 assert_eq!(api.price, dec!(100.5));
224 assert_eq!(api.maker_address, test_wallet(1));
225 assert_eq!(api.taker_address, test_wallet(2));
226 assert_eq!(api.maker_fee, dec!(0.5));
227 assert_eq!(api.taker_fee, dec!(0.3));
228 assert_eq!(api.timestamp, 1700000000000);
229 assert_eq!(api.created_at, created);
230 assert_eq!(
232 api.size,
233 to_human_readable_decimal("BTC-20260115-95000-C", dec!(1000000))
234 );
235 }
236
237 #[test]
238 fn trade_record_to_api_none_created_at_uses_fallback() {
239 let record = TradeRecord {
240 trade_id: 1,
241 symbol: "ETH-20260115-4000-P".into(),
242 price: dec!(50),
243 size: dec!(500000),
244 maker_address: test_wallet(3),
245 taker_address: test_wallet(4),
246 maker_fee: dec!(0),
247 taker_fee: dec!(0),
248 timestamp: 0,
249 created_at: None,
250 };
251 let api: TradeApiResponse = record.into();
252 assert!(api.created_at.timestamp() > 0);
254 }
255
256 #[test]
257 fn fill_record_to_api_response() {
258 let ts = NaiveDateTime::from_timestamp_opt(1700000000, 0).unwrap();
259 let created = DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc);
260 let record = FillRecord {
261 fill_id: Some(99),
262 trade_id: 42,
263 wallet_address: test_wallet(5),
264 symbol: "BTC-20260115-95000-C".into(),
265 price: dec!(100),
266 size: dec!(500000),
267 fee: dec!(0.25),
268 is_taker: true,
269 timestamp: 1700000000000,
270 builder_code_address: Some(test_wallet(6)),
271 builder_code_fee: Some(dec!(0.05)),
272 realized_pnl: Some(dec!(10)),
273 underlying_notional: Some(dec!(50000000)),
274 created_at: Some(created),
275 };
276 let api: FillApiResponse = record.into();
277 assert_eq!(api.fill_id, 99);
278 assert_eq!(api.trade_id, 42);
279 assert_eq!(api.wallet_address, test_wallet(5));
280 assert_eq!(api.price, dec!(100));
281 assert!(api.is_taker);
282 assert_eq!(api.builder_code_address, Some(test_wallet(6)));
283 assert_eq!(api.builder_code_fee, Some(dec!(0.05)));
284 assert_eq!(api.realized_pnl, Some(dec!(10)));
285 assert!(api.explorer_url.is_none());
286 assert_eq!(api.created_at, created);
287 }
288
289 #[test]
290 fn fill_record_to_api_none_fill_id_defaults_zero() {
291 let record = FillRecord {
292 fill_id: None,
293 trade_id: 1,
294 wallet_address: test_wallet(7),
295 symbol: "ETH-20260115-4000-P".into(),
296 price: dec!(0),
297 size: dec!(0),
298 fee: dec!(0),
299 is_taker: false,
300 timestamp: 0,
301 builder_code_address: None,
302 builder_code_fee: None,
303 realized_pnl: None,
304 underlying_notional: None,
305 created_at: None,
306 };
307 let api: FillApiResponse = record.into();
308 assert_eq!(api.fill_id, 0);
309 assert!(api.builder_code_address.is_none());
310 assert!(api.builder_code_fee.is_none());
311 assert!(api.realized_pnl.is_none());
312 }
313
314 #[test]
315 fn order_record_to_api_response() {
316 let ts = NaiveDateTime::from_timestamp_opt(1700000000, 0).unwrap();
317 let updated = DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc);
318 let record = OrderRecord {
319 order_id: 123,
320 wallet_address: test_wallet(8),
321 symbol: "BTC-20260115-95000-C".into(),
322 side: "buy".into(),
323 price: dec!(5.5),
324 size: dec!(2000000),
325 filled_size: dec!(1000000),
326 status: "PARTIALLY_FILLED".into(),
327 tif: "GTC".into(),
328 client_id: Some("my-order-1".into()),
329 is_perp: false,
330 underlying: Some("BTC".into()),
331 reduce_only: Some(false),
332 timestamp: 1700000000000,
333 created_at: Some(updated),
334 mmp_enabled: true,
335 };
336 let api: Order = record.into();
337 assert_eq!(api.order_id, 123);
338 assert_eq!(api.wallet_address, test_wallet(8));
339 assert_eq!(api.side, "buy");
340 assert_eq!(api.price, dec!(5.5));
341 assert_eq!(api.tif, "GTC");
342 assert_eq!(api.status, Some("PARTIALLY_FILLED".into()));
343 assert_eq!(api.created_at, 1700000000000);
344 assert_eq!(api.updated_at, Some(updated));
345 assert!(api.mmp_enabled);
346 let expected_size = to_human_readable_decimal("BTC-20260115-95000-C", dec!(2000000));
348 let expected_filled = to_human_readable_decimal("BTC-20260115-95000-C", dec!(1000000));
349 assert_eq!(api.size, expected_size);
350 assert_eq!(api.filled_size, Some(expected_filled));
351 }
352}