1use serde::{Deserialize, Deserializer, Serialize};
2use std::fmt;
3
4pub mod order_action {
9 pub const BUY: &str = "ORDER_ACTION_BUY";
10 pub const SELL: &str = "ORDER_ACTION_SELL";
11}
12
13pub mod order_type {
14 pub const LIMIT: &str = "ORDER_TYPE_LIMIT";
15}
16
17pub mod tif {
18 pub const GTC: &str = "TIME_IN_FORCE_GOOD_TILL_CANCEL";
19 pub const GTD: &str = "TIME_IN_FORCE_GOOD_TILL_DATE";
20 pub const FAK: &str = "TIME_IN_FORCE_IMMEDIATE_OR_CANCEL";
21 pub const FOK: &str = "TIME_IN_FORCE_FILL_OR_KILL";
22}
23
24pub mod outcome {
25 pub const LONG: &str = "LONG";
26 pub const SHORT: &str = "SHORT";
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[non_exhaustive]
36pub enum OrderAction {
37 #[serde(rename = "ORDER_ACTION_BUY")]
38 Buy,
39 #[serde(rename = "ORDER_ACTION_SELL")]
40 Sell,
41}
42
43impl fmt::Display for OrderAction {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::Buy => f.write_str("ORDER_ACTION_BUY"),
47 Self::Sell => f.write_str("ORDER_ACTION_SELL"),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[non_exhaustive]
55pub enum OrderSide {
56 #[serde(rename = "LONG")]
57 Long,
58 #[serde(rename = "SHORT")]
59 Short,
60}
61
62impl fmt::Display for OrderSide {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::Long => f.write_str("LONG"),
66 Self::Short => f.write_str("SHORT"),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
73#[non_exhaustive]
74pub enum OrderType {
75 #[serde(rename = "ORDER_TYPE_LIMIT")]
76 Limit,
77}
78
79impl fmt::Display for OrderType {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.write_str("ORDER_TYPE_LIMIT")
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[non_exhaustive]
88pub enum TimeInForce {
89 #[serde(rename = "TIME_IN_FORCE_GOOD_TILL_CANCEL")]
91 GoodTillCancel,
92 #[serde(rename = "TIME_IN_FORCE_GOOD_TILL_DATE")]
94 GoodTillDate,
95 #[serde(rename = "TIME_IN_FORCE_IMMEDIATE_OR_CANCEL")]
97 ImmediateOrCancel,
98 #[serde(rename = "TIME_IN_FORCE_FILL_OR_KILL")]
100 FillOrKill,
101}
102
103impl fmt::Display for TimeInForce {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 let s = match self {
106 Self::GoodTillCancel => "TIME_IN_FORCE_GOOD_TILL_CANCEL",
107 Self::GoodTillDate => "TIME_IN_FORCE_GOOD_TILL_DATE",
108 Self::ImmediateOrCancel => "TIME_IN_FORCE_IMMEDIATE_OR_CANCEL",
109 Self::FillOrKill => "TIME_IN_FORCE_FILL_OR_KILL",
110 };
111 f.write_str(s)
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
121#[serde(rename_all = "lowercase")]
122#[non_exhaustive]
123pub enum MarketStatus {
124 Open,
125 Closed,
126 Resolved,
127 #[serde(other)]
129 Unknown,
130}
131
132impl MarketStatus {
133 pub fn from_api_str(raw: &str) -> Self {
138 match raw.trim().to_ascii_lowercase().as_str() {
139 "open" => Self::Open,
140 "closed" => Self::Closed,
141 "resolved" => Self::Resolved,
142 _ => Self::Unknown,
143 }
144 }
145}
146
147impl std::str::FromStr for MarketStatus {
148 type Err = std::convert::Infallible;
149
150 fn from_str(s: &str) -> Result<Self, Self::Err> {
151 Ok(Self::from_api_str(s))
152 }
153}
154
155#[derive(Debug, Clone, Deserialize)]
160pub struct HealthResponse {
161 #[serde(default)]
162 pub status: String,
163 #[serde(default)]
164 pub timestamp: String,
165}
166
167#[derive(Debug, Clone, Deserialize)]
168pub struct MarketsResponse {
169 #[serde(default)]
170 pub markets: Vec<UsMarket>,
171}
172
173#[derive(Debug, Clone, Deserialize)]
174pub struct UsMarket {
175 #[serde(default)]
176 pub id: String,
177 #[serde(default)]
178 pub slug: String,
179 #[serde(default)]
180 pub question: String,
181 #[serde(default)]
182 pub status: String,
183 #[serde(default)]
184 pub category: String,
185 #[serde(default, rename = "startDate")]
186 pub start_date: String,
187 #[serde(default, rename = "endDate")]
188 pub end_date: String,
189 #[serde(default)]
190 pub description: String,
191 #[serde(default)]
192 pub active: bool,
193 #[serde(default)]
194 pub closed: bool,
195 #[serde(default, rename = "marketType")]
196 pub market_type: String,
197 #[serde(default, rename = "marketSides")]
198 pub market_sides: Vec<MarketSide>,
199 #[serde(default)]
200 pub instruments: Vec<serde_json::Value>,
201 #[serde(default, deserialize_with = "json_encoded_array")]
206 pub outcomes: Vec<String>,
207 #[serde(
211 default,
212 rename = "outcomePrices",
213 deserialize_with = "json_encoded_array"
214 )]
215 pub outcome_prices: Vec<String>,
216}
217
218impl UsMarket {
219 pub fn parsed_status(&self) -> MarketStatus {
222 MarketStatus::from_api_str(&self.status)
223 }
224}
225
226fn json_encoded_array<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
239where
240 D: Deserializer<'de>,
241{
242 #[derive(Deserialize)]
243 #[serde(untagged)]
244 enum Encoding {
245 Array(Vec<serde_json::Value>),
246 Encoded(String),
247 }
248
249 let items = match Option::<Encoding>::deserialize(deserializer)? {
250 None => return Ok(Vec::new()),
251 Some(Encoding::Array(items)) => items,
252 Some(Encoding::Encoded(raw)) => {
253 let trimmed = raw.trim();
254 if trimmed.is_empty() {
255 return Ok(Vec::new());
256 }
257 serde_json::from_str(trimmed).map_err(serde::de::Error::custom)?
258 }
259 };
260
261 Ok(items.into_iter().map(render_scalar).collect())
262}
263
264fn render_scalar(value: serde_json::Value) -> String {
267 match value {
268 serde_json::Value::String(text) => text,
269 other => other.to_string(),
270 }
271}
272
273#[derive(Debug, Clone, Deserialize)]
274pub struct MarketSide {
275 #[serde(default)]
276 pub id: String,
277 #[serde(default)]
278 pub identifier: String,
279 #[serde(default)]
280 pub description: String,
281 #[serde(default)]
282 pub price: String,
283 #[serde(default)]
284 pub long: bool,
285 #[serde(default, rename = "marketSideType")]
286 pub market_side_type: String,
287 #[serde(default)]
288 pub team: Option<serde_json::Value>,
289 #[serde(default)]
290 pub player: Option<serde_json::Value>,
291 #[serde(flatten)]
292 pub extra: std::collections::HashMap<String, serde_json::Value>,
293}
294
295#[derive(Debug, Clone, Serialize)]
296pub struct PlaceOrderRequest {
297 pub symbol: String,
298 pub action: OrderAction,
299 #[serde(rename = "outcomeSide")]
300 pub outcome_side: OrderSide,
301 #[serde(rename = "type")]
302 pub order_type: OrderType,
303 pub price: Money,
304 pub quantity: u64,
305 pub tif: TimeInForce,
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub client_order_id: Option<String>,
308 #[serde(skip_serializing_if = "std::ops::Not::not")]
309 pub post_only: bool,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 pub expires_at: Option<u64>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct Money {
316 pub value: String,
317 pub currency: String,
318}
319
320#[derive(Debug, Clone, Deserialize)]
321pub struct PlaceOrderResponse {
322 pub order_id: String,
323 #[serde(default)]
324 pub client_order_id: Option<String>,
325 #[serde(default)]
326 pub status: String,
327 #[serde(default)]
328 pub filled_quantity: u64,
329 #[serde(default)]
330 pub remaining_quantity: u64,
331 #[serde(default)]
332 pub created_at: String,
333}
334
335#[derive(Debug, Clone, Serialize)]
336pub struct BatchedOrderRequest {
337 pub orders: Vec<PlaceOrderRequest>,
338 pub atomic: bool,
339}
340
341#[derive(Debug, Clone, Deserialize)]
342pub struct BatchedOrderResponse {
343 #[serde(default)]
344 pub orders: Vec<PlaceOrderResponse>,
345}
346
347#[derive(Debug, Clone, Deserialize)]
348pub struct CancelOrderResponse {
349 pub order_id: String,
350 #[serde(default)]
351 pub status: String,
352 #[serde(default)]
353 pub cancelled_at: Option<String>,
354}
355
356#[derive(Debug, Clone, Deserialize)]
357pub struct PortfolioPositionsResponse {
358 #[serde(default)]
359 pub positions: std::collections::HashMap<String, UsPosition>,
360 #[serde(default)]
361 pub next_cursor: String,
362 #[serde(default)]
363 pub eof: bool,
364 #[serde(default, rename = "availablePositions")]
365 pub available_positions: Vec<UsPosition>,
366}
367
368#[derive(Debug, Clone, Deserialize)]
369pub struct UsPosition {
370 #[serde(default)]
371 pub symbol: String,
372 #[serde(default)]
373 pub quantity: i64,
374 #[serde(default, rename = "avgEntryPrice")]
375 pub avg_entry_price: String,
376 #[serde(default, rename = "unrealizedPnl")]
377 pub unrealized_pnl: Option<String>,
378}
379
380#[derive(Debug, Clone, Deserialize)]
381pub struct PortfolioActivitiesResponse {
382 #[serde(default)]
383 pub activities: Vec<serde_json::Value>,
384 #[serde(default)]
385 pub next_cursor: Option<String>,
386}
387
388#[derive(Debug, Clone, Deserialize)]
389pub struct AccountBalancesResponse {
390 #[serde(default)]
391 pub balances: Vec<UserBalance>,
392}
393
394#[derive(Debug, Clone, Deserialize)]
395pub struct UserBalance {
396 #[serde(default, rename = "currentBalance")]
397 pub current_balance: f64,
398 #[serde(default)]
399 pub currency: String,
400 #[serde(default, rename = "lastUpdated")]
401 pub last_updated: Option<String>,
402 #[serde(default, rename = "buyingPower")]
403 pub buying_power: f64,
404 #[serde(default, rename = "assetNotional")]
405 pub asset_notional: Option<f64>,
406 #[serde(default, rename = "assetAvailable")]
407 pub asset_available: Option<f64>,
408 #[serde(default, rename = "pendingCredit")]
409 pub pending_credit: Option<f64>,
410 #[serde(default, rename = "openOrders")]
411 pub open_orders: Option<f64>,
412 #[serde(default, rename = "unsettledFunds")]
413 pub unsettled_funds: Option<f64>,
414 #[serde(default, rename = "marginRequirement")]
415 pub margin_requirement: Option<f64>,
416 #[serde(default, rename = "balanceReservation")]
417 pub balance_reservation: Option<f64>,
418}
419
420#[derive(Debug, Clone, Serialize, Default)]
421pub struct CancelOrderParams {
422 #[serde(skip_serializing_if = "Option::is_none")]
423 pub quantity: Option<u64>,
424}
425
426#[derive(Debug, Clone, Serialize, Default)]
427pub struct CancelAllOrdersParams {
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub symbol: Option<String>,
430}
431
432#[derive(Debug, Clone, Deserialize)]
433pub struct CancelAllOrdersResponse {
434 #[serde(default)]
435 pub cancelled: Vec<String>,
436}
437
438#[derive(Debug, Clone, Serialize)]
439pub struct ModifyOrderRequest {
440 pub price: Money,
441 pub quantity: u64,
442}
443
444#[derive(Debug, Clone, Serialize)]
445pub struct PreviewOrderRequest {
446 pub symbol: String,
447 pub action: OrderAction,
448 #[serde(rename = "outcomeSide")]
449 pub outcome_side: OrderSide,
450 #[serde(rename = "type")]
451 pub order_type: OrderType,
452 pub price: Money,
453 pub quantity: u64,
454}
455
456#[derive(Debug, Clone, Deserialize)]
457pub struct PreviewOrderResponse {
458 #[serde(default)]
459 pub estimate: serde_json::Value,
460}
461
462#[derive(Debug, Clone, Serialize)]
463pub struct ClosePositionRequest {
464 pub symbol: String,
465 pub quantity: u64,
466}
467
468#[derive(Debug, Clone, Deserialize)]
469pub struct ClosePositionResponse {
470 #[serde(default)]
471 pub status: String,
472 #[serde(default)]
473 pub order_id: Option<String>,
474}
475
476#[derive(Debug, Clone, Deserialize)]
477pub struct GetOpenOrdersResponse {
478 #[serde(default)]
479 pub orders: Vec<PlaceOrderResponse>,
480}
481
482#[derive(Debug, Clone, Deserialize)]
484pub struct EventsResponse {
485 #[serde(default)]
486 pub events: Vec<UsEvent>,
487}
488
489#[derive(Debug, Clone, Deserialize)]
490pub struct UsEvent {
491 #[serde(default)]
492 pub id: String,
493 #[serde(default)]
494 pub slug: String,
495 #[serde(default)]
496 pub title: String,
497 #[serde(default)]
498 pub description: Option<String>,
499 #[serde(default)]
500 pub category: String,
501 #[serde(default)]
502 pub start_date: Option<String>,
503 #[serde(default)]
504 pub end_date: Option<String>,
505 #[serde(flatten)]
506 pub extra: std::collections::HashMap<String, serde_json::Value>,
507}
508
509#[derive(Debug, Clone, Deserialize)]
511pub struct OrderBook {
512 #[serde(default)]
513 pub bids: Vec<PriceLevel>,
514 #[serde(default)]
515 pub asks: Vec<PriceLevel>,
516}
517
518#[derive(Debug, Clone, Deserialize, Serialize)]
519pub struct PriceLevel {
520 pub price: String,
521 pub quantity: String,
522}
523
524#[derive(Debug, Clone, Deserialize)]
525pub struct BestBidOffer {
526 #[serde(default)]
527 pub bid: Option<PriceLevel>,
528 #[serde(default)]
529 pub ask: Option<PriceLevel>,
530}
531
532#[derive(Debug, Clone, Deserialize)]
533pub struct SettlementPrice {
534 #[serde(default)]
535 pub symbol: String,
536 #[serde(default)]
537 pub price: String,
538 #[serde(default)]
539 pub timestamp: String,
540}
541
542#[derive(Debug, Clone, Deserialize)]
544pub struct SearchResults {
545 #[serde(default)]
546 pub markets: Vec<UsMarket>,
547 #[serde(default)]
548 pub events: Vec<UsEvent>,
549}
550
551#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn outcomes_parse_from_the_gateway_double_encoded_form() {
562 let market: UsMarket = serde_json::from_str(
564 r#"{"slug":"x","outcomes":"[\"Titans\",\"Chargers\"]","outcomePrices":"[\"1\",\"0\"]"}"#,
565 )
566 .expect("should deserialize");
567 assert_eq!(market.outcomes, ["Titans", "Chargers"]);
568 assert_eq!(market.outcome_prices, ["1", "0"]);
569 }
570
571 #[test]
572 fn outcomes_parse_from_a_plain_array() {
573 let market: UsMarket =
575 serde_json::from_str(r#"{"outcomes":["Yes","No"],"outcomePrices":["0.6","0.4"]}"#)
576 .expect("should deserialize");
577 assert_eq!(market.outcomes, ["Yes", "No"]);
578 assert_eq!(market.outcome_prices, ["0.6", "0.4"]);
579 }
580
581 #[test]
582 fn numeric_elements_are_rendered_rather_than_rejected() {
583 let market: UsMarket =
584 serde_json::from_str(r#"{"outcomePrices":"[1,0.55]"}"#).expect("should deserialize");
585 assert_eq!(market.outcome_prices, ["1", "0.55"]);
586 }
587
588 #[test]
589 fn absent_null_and_empty_outcomes_all_become_an_empty_vec() {
590 for body in [r#"{}"#, r#"{"outcomes":null}"#, r#"{"outcomes":""}"#] {
591 let market: UsMarket =
592 serde_json::from_str(body).unwrap_or_else(|err| panic!("{body}: {err}"));
593 assert!(market.outcomes.is_empty(), "{body}");
594 }
595 }
596
597 #[test]
598 fn a_malformed_encoding_is_an_error_not_a_silent_empty() {
599 let err = serde_json::from_str::<UsMarket>(r#"{"outcomes":"[\"unterminated"}"#)
600 .expect_err("should fail");
601 assert!(
602 err.to_string().contains("EOF") || err.to_string().contains("control"),
603 "unexpected error: {err}"
604 );
605 }
606
607 #[test]
608 fn market_status_parses_case_insensitively() {
609 assert_eq!(MarketStatus::from_api_str("open"), MarketStatus::Open);
610 assert_eq!(MarketStatus::from_api_str("OPEN"), MarketStatus::Open);
611 assert_eq!(
612 MarketStatus::from_api_str(" Closed "),
613 MarketStatus::Closed
614 );
615 assert_eq!(
616 MarketStatus::from_api_str("RESOLVED"),
617 MarketStatus::Resolved
618 );
619 }
620
621 #[test]
622 fn market_status_falls_back_to_unknown() {
623 assert_eq!(MarketStatus::from_api_str("halted"), MarketStatus::Unknown);
625 assert_eq!(MarketStatus::from_api_str(""), MarketStatus::Unknown);
626 }
627
628 #[test]
629 fn parsed_status_reads_the_raw_field() {
630 let json = r#"{"id": "m1", "status": "OPEN"}"#;
631 let market: UsMarket = serde_json::from_str(json).expect("deserialize");
632 assert_eq!(market.status, "OPEN");
634 assert_eq!(market.parsed_status(), MarketStatus::Open);
635 }
636
637 #[test]
638 fn market_sides_deserialize_into_typed_values() {
639 let json = r#"{
640 "id": "m1",
641 "marketSides": [
642 {"id": "s1", "identifier": "YES", "price": "0.62", "long": true,
643 "marketSideType": "BINARY", "unmodelledField": 7}
644 ]
645 }"#;
646 let market: UsMarket = serde_json::from_str(json).expect("deserialize");
647 assert_eq!(market.market_sides.len(), 1);
648
649 let side = &market.market_sides[0];
650 assert_eq!(side.identifier, "YES");
651 assert_eq!(side.price, "0.62");
652 assert!(side.long);
653 assert_eq!(
655 side.extra.get("unmodelledField"),
656 Some(&serde_json::json!(7))
657 );
658 }
659
660 #[test]
661 fn market_sides_tolerate_missing_fields() {
662 let json = r#"{"id": "m1", "marketSides": [{}]}"#;
664 let market: UsMarket = serde_json::from_str(json).expect("deserialize");
665 assert_eq!(market.market_sides.len(), 1);
666 assert_eq!(market.market_sides[0].identifier, "");
667 }
668}