1use std::fmt::Debug;
26
27use ahash::AHashMap;
28use nautilus_core::{
29 serialization::{deserialize_decimal, deserialize_optional_decimal},
30 string::secret::SecretString,
31};
32use rust_decimal::Decimal;
33use serde::{Deserialize, Serialize};
34use ustr::Ustr;
35use zeroize::ZeroizeOnDrop;
36
37use crate::common::{
38 enums::{
39 BetDelayModel, BetStatus, BetTargetType, BetfairOrderStatus, BetfairOrderType, BetfairSide,
40 BetfairTimeInForce, CertLoginStatus, ExecutionReportErrorCode, ExecutionReportStatus,
41 GroupBy, InstructionReportErrorCode, InstructionReportStatus, MarketBettingType,
42 MarketProjection, MarketSort, OrderBy, OrderProjection, PersistenceType, PriceLadderType,
43 SortDir,
44 },
45 types::{
46 BetId, CompetitionId, CustomerOrderRef, CustomerStrategyRef, EventId, EventTypeId,
47 Handicap, MarketId, SelectionId, deserialize_optional_string_lenient,
48 deserialize_optional_u32_lenient,
49 },
50};
51
52#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
54#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
55pub enum LoginStatus {
56 Success,
57 LimitedAccess,
58 LoginRestricted,
59 Fail,
60}
61
62#[derive(Debug, Clone, Deserialize, ZeroizeOnDrop)]
64pub struct LoginResponse {
65 pub token: SecretString,
66 pub product: String,
67 #[zeroize(skip)]
68 pub status: LoginStatus,
69 pub error: Option<String>,
70}
71
72#[derive(Debug, Clone, Deserialize, ZeroizeOnDrop)]
76#[serde(rename_all = "camelCase")]
77pub struct CertLoginResponse {
78 pub session_token: Option<SecretString>,
79 #[zeroize(skip)]
80 pub login_status: CertLoginStatus,
81}
82
83#[derive(Debug, Clone, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct AccountDetailsResponse {
87 pub currency_code: Option<Ustr>,
88 pub first_name: Option<String>,
89 pub last_name: Option<String>,
90 pub locale_code: Option<Ustr>,
91 pub region: Option<Ustr>,
92 pub timezone: Option<Ustr>,
93 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
94 pub discount_rate: Option<Decimal>,
95 pub points_balance: Option<i64>,
96 pub country_code: Option<Ustr>,
97}
98
99#[derive(Debug, Clone, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct AccountFundsResponse {
103 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
104 pub available_to_bet_balance: Option<Decimal>,
105 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
106 pub exposure: Option<Decimal>,
107 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
108 pub retained_commission: Option<Decimal>,
109 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
110 pub exposure_limit: Option<Decimal>,
111 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
112 pub discount_rate: Option<Decimal>,
113 pub points_balance: Option<i64>,
114 pub wallet: Option<Ustr>,
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
119pub struct TimeRange {
120 pub from: Option<String>,
121 pub to: Option<String>,
122}
123
124#[derive(Debug, Clone, Deserialize)]
126pub struct PriceSize {
127 #[serde(deserialize_with = "deserialize_decimal")]
128 pub price: Decimal,
129 #[serde(deserialize_with = "deserialize_decimal")]
130 pub size: Decimal,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct MarketVersion {
136 pub version: Option<i64>,
137}
138
139#[derive(Debug, Clone, Deserialize)]
141pub struct EventType {
142 #[serde(default, deserialize_with = "deserialize_optional_string_lenient")]
143 pub id: Option<EventTypeId>,
144 pub name: Option<Ustr>,
145}
146
147#[derive(Debug, Clone, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct Event {
151 #[serde(default, deserialize_with = "deserialize_optional_string_lenient")]
152 pub id: Option<EventId>,
153 pub name: Option<String>,
154 pub country_code: Option<Ustr>,
155 pub timezone: Option<Ustr>,
156 pub venue: Option<Ustr>,
157 pub open_date: Option<String>,
158}
159
160#[derive(Debug, Clone, Deserialize)]
162pub struct Competition {
163 #[serde(default, deserialize_with = "deserialize_optional_string_lenient")]
164 pub id: Option<CompetitionId>,
165 pub name: Option<String>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct RunnerId {
172 pub market_id: MarketId,
173 pub selection_id: SelectionId,
174 pub handicap: Option<Handicap>,
175}
176
177#[derive(Debug, Clone, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct MarketCatalogue {
181 pub market_id: MarketId,
182 pub market_name: String,
183 pub market_start_time: Option<String>,
184 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
185 pub total_matched: Option<Decimal>,
186 pub event_type: Option<EventType>,
187 pub competition: Option<Competition>,
188 pub description: Option<MarketDescription>,
189 pub event: Option<Event>,
190 pub runners: Option<Vec<RunnerCatalog>>,
191}
192
193#[derive(Debug, Clone, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct MarketDescription {
197 pub betting_type: MarketBettingType,
198 pub bsp_market: bool,
199 pub clarifications: Option<String>,
200 pub discount_allowed: bool,
201 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
202 pub each_way_divisor: Option<Decimal>,
203 #[serde(deserialize_with = "deserialize_decimal")]
204 pub market_base_rate: Decimal,
205 pub market_time: String,
206 pub market_type: Ustr,
207 pub persistence_enabled: bool,
208 pub race_type: Option<Ustr>,
209 pub regulator: Ustr,
210 pub rules: Option<String>,
211 pub rules_has_date: Option<bool>,
212 pub settle_time: Option<String>,
213 pub suspend_time: String,
214 pub turn_in_play_enabled: bool,
215 pub wallet: Option<Ustr>,
216 pub bet_delay_models: Option<Vec<BetDelayModel>>,
217 pub line_range_info: Option<LineRangeInfo>,
218 pub price_ladder_description: Option<PriceLadderDescription>,
219}
220
221#[derive(Debug, Clone, Deserialize)]
223pub struct PriceLadderDescription {
224 #[serde(rename = "type")]
225 pub ladder_type: Option<PriceLadderType>,
226}
227
228#[derive(Debug, Clone, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct LineRangeInfo {
232 #[serde(deserialize_with = "deserialize_decimal")]
233 pub max_unit_value: Decimal,
234 #[serde(deserialize_with = "deserialize_decimal")]
235 pub min_unit_value: Decimal,
236 #[serde(deserialize_with = "deserialize_decimal")]
237 pub interval: Decimal,
238}
239
240#[derive(Debug, Clone, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct RunnerCatalog {
244 pub selection_id: SelectionId,
245 pub runner_name: String,
246 pub handicap: Handicap,
247 pub sort_priority: Option<u32>,
248 pub metadata: Option<AHashMap<String, serde_json::Value>>,
254}
255
256#[derive(Debug, Clone, Default, Serialize, Deserialize)]
258#[serde(rename_all = "camelCase")]
259pub struct MarketFilter {
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub bsp_only: Option<bool>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub competition_ids: Option<Vec<CompetitionId>>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub event_ids: Option<Vec<EventId>>,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub event_type_ids: Option<Vec<EventTypeId>>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub in_play_only: Option<bool>,
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub market_betting_types: Option<Vec<MarketBettingType>>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub market_countries: Option<Vec<Ustr>>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub market_ids: Option<Vec<MarketId>>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub market_start_time: Option<TimeRange>,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub market_type_codes: Option<Vec<Ustr>>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub race_types: Option<Vec<Ustr>>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub text_query: Option<String>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub turn_in_play_enabled: Option<bool>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub venues: Option<Vec<Ustr>>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub with_orders: Option<Vec<String>>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub struct LimitOrder {
296 pub size: Decimal,
297 pub price: Decimal,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 pub persistence_type: Option<PersistenceType>,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub time_in_force: Option<BetfairTimeInForce>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub min_fill_size: Option<Decimal>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub bet_target_type: Option<BetTargetType>,
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub bet_target_size: Option<Decimal>,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct LimitOnCloseOrder {
313 pub liability: Decimal,
314 pub price: Decimal,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct MarketOnCloseOrder {
320 pub liability: Decimal,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub struct PlaceInstruction {
327 pub order_type: BetfairOrderType,
328 pub selection_id: SelectionId,
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub handicap: Option<Handicap>,
331 pub side: BetfairSide,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub limit_order: Option<LimitOrder>,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub limit_on_close_order: Option<LimitOnCloseOrder>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub market_on_close_order: Option<MarketOnCloseOrder>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub customer_order_ref: Option<String>,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct CancelInstruction {
346 pub bet_id: BetId,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub size_reduction: Option<Decimal>,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353#[serde(rename_all = "camelCase")]
354pub struct ReplaceInstruction {
355 pub bet_id: BetId,
356 pub new_price: Decimal,
357}
358
359#[derive(Debug, Clone, Serialize)]
361#[serde(rename_all = "camelCase")]
362pub struct PlaceOrdersParams {
363 pub market_id: MarketId,
364 pub instructions: Vec<PlaceInstruction>,
365 #[serde(skip_serializing_if = "Option::is_none")]
366 pub customer_ref: Option<String>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub market_version: Option<MarketVersion>,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub customer_strategy_ref: Option<CustomerStrategyRef>,
371}
372
373#[derive(Debug, Clone, Serialize)]
375#[serde(rename_all = "camelCase")]
376pub struct CancelOrdersParams {
377 #[serde(skip_serializing_if = "Option::is_none")]
378 pub market_id: Option<MarketId>,
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub instructions: Option<Vec<CancelInstruction>>,
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub customer_ref: Option<String>,
383}
384
385#[derive(Debug, Clone, Serialize)]
387#[serde(rename_all = "camelCase")]
388pub struct ReplaceOrdersParams {
389 pub market_id: MarketId,
390 pub instructions: Vec<ReplaceInstruction>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub customer_ref: Option<String>,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub market_version: Option<MarketVersion>,
395}
396
397#[derive(Debug, Clone, Serialize)]
399#[serde(rename_all = "camelCase")]
400pub struct ListMarketCatalogueParams {
401 pub filter: MarketFilter,
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub market_projection: Option<Vec<MarketProjection>>,
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub sort: Option<MarketSort>,
406 #[serde(skip_serializing_if = "Option::is_none")]
407 pub max_results: Option<u32>,
408 #[serde(skip_serializing_if = "Option::is_none")]
409 pub locale: Option<String>,
410}
411
412#[derive(Debug, Clone, Serialize)]
414#[serde(rename_all = "camelCase")]
415pub struct ListCurrentOrdersParams {
416 #[serde(skip_serializing_if = "Option::is_none")]
417 pub bet_ids: Option<Vec<BetId>>,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 pub market_ids: Option<Vec<MarketId>>,
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub order_projection: Option<OrderProjection>,
422 #[serde(skip_serializing_if = "Option::is_none")]
423 pub customer_order_refs: Option<Vec<CustomerOrderRef>>,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub customer_strategy_refs: Option<Vec<CustomerStrategyRef>>,
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub date_range: Option<TimeRange>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub order_by: Option<OrderBy>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub sort_dir: Option<SortDir>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub from_record: Option<u32>,
434 #[serde(skip_serializing_if = "Option::is_none")]
435 pub record_count: Option<u32>,
436}
437
438#[derive(Debug, Clone, Serialize)]
440#[serde(rename_all = "camelCase")]
441pub struct ListClearedOrdersParams {
442 pub bet_status: BetStatus,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 pub event_type_ids: Option<Vec<EventTypeId>>,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub event_ids: Option<Vec<EventId>>,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub market_ids: Option<Vec<MarketId>>,
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub runner_ids: Option<Vec<RunnerId>>,
451 #[serde(skip_serializing_if = "Option::is_none")]
452 pub bet_ids: Option<Vec<BetId>>,
453 #[serde(skip_serializing_if = "Option::is_none")]
454 pub customer_order_refs: Option<Vec<CustomerOrderRef>>,
455 #[serde(skip_serializing_if = "Option::is_none")]
456 pub customer_strategy_refs: Option<Vec<CustomerStrategyRef>>,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub side: Option<BetfairSide>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub settled_date_range: Option<TimeRange>,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub group_by: Option<GroupBy>,
463 #[serde(skip_serializing_if = "Option::is_none")]
464 pub include_item_description: Option<bool>,
465 #[serde(skip_serializing_if = "Option::is_none")]
466 pub locale: Option<String>,
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub from_record: Option<u32>,
469 #[serde(skip_serializing_if = "Option::is_none")]
470 pub record_count: Option<u32>,
471}
472
473#[derive(Debug, Clone, Deserialize)]
475#[serde(rename_all = "camelCase")]
476pub struct PlaceExecutionReport {
477 pub customer_ref: Option<String>,
478 pub status: ExecutionReportStatus,
479 pub error_code: Option<ExecutionReportErrorCode>,
480 pub error_message: Option<String>,
481 pub market_id: Option<MarketId>,
482 pub instruction_reports: Option<Vec<PlaceInstructionReport>>,
483}
484
485#[derive(Debug, Clone, Deserialize)]
487#[serde(rename_all = "camelCase")]
488pub struct PlaceInstructionReport {
489 pub status: InstructionReportStatus,
490 pub error_code: Option<InstructionReportErrorCode>,
491 pub error_message: Option<String>,
492 pub order_status: Option<BetfairOrderStatus>,
493 pub instruction: Option<PlaceInstruction>,
494 pub bet_id: Option<BetId>,
495 pub placed_date: Option<String>,
496 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
497 pub average_price_matched: Option<Decimal>,
498 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
499 pub size_matched: Option<Decimal>,
500}
501
502#[derive(Debug, Clone, Deserialize)]
504#[serde(rename_all = "camelCase")]
505pub struct CancelExecutionReport {
506 pub customer_ref: Option<String>,
507 pub status: ExecutionReportStatus,
508 pub error_code: Option<ExecutionReportErrorCode>,
509 pub error_message: Option<String>,
510 pub market_id: Option<MarketId>,
511 pub instruction_reports: Option<Vec<CancelInstructionReport>>,
512}
513
514#[derive(Debug, Clone, Deserialize)]
516#[serde(rename_all = "camelCase")]
517pub struct CancelInstructionReport {
518 pub status: InstructionReportStatus,
519 pub error_code: Option<InstructionReportErrorCode>,
520 pub error_message: Option<String>,
521 pub instruction: Option<CancelInstruction>,
522 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
523 pub size_cancelled: Option<Decimal>,
524 pub cancelled_date: Option<String>,
525}
526
527#[derive(Debug, Clone, Deserialize)]
529#[serde(rename_all = "camelCase")]
530pub struct ReplaceExecutionReport {
531 pub customer_ref: Option<String>,
532 pub status: ExecutionReportStatus,
533 pub error_code: Option<ExecutionReportErrorCode>,
534 pub error_message: Option<String>,
535 pub market_id: Option<MarketId>,
536 pub instruction_reports: Option<Vec<ReplaceInstructionReport>>,
537}
538
539#[derive(Debug, Clone, Deserialize)]
541#[serde(rename_all = "camelCase")]
542pub struct ReplaceInstructionReport {
543 pub status: InstructionReportStatus,
544 pub error_code: Option<InstructionReportErrorCode>,
545 pub error_message: Option<String>,
546 pub cancel_instruction_report: Option<CancelInstructionReport>,
547 pub place_instruction_report: Option<PlaceInstructionReport>,
548}
549
550#[derive(Debug, Clone, Deserialize)]
552#[serde(rename_all = "camelCase")]
553pub struct CurrentOrderSummary {
554 pub bet_id: BetId,
555 pub market_id: MarketId,
556 pub selection_id: SelectionId,
557 pub handicap: Handicap,
558 pub price_size: PriceSize,
559 #[serde(deserialize_with = "deserialize_decimal")]
560 pub bsp_liability: Decimal,
561 pub side: BetfairSide,
562 pub status: BetfairOrderStatus,
563 pub persistence_type: PersistenceType,
564 pub order_type: BetfairOrderType,
565 pub placed_date: String,
566 pub matched_date: Option<String>,
567 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
568 pub average_price_matched: Option<Decimal>,
569 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
570 pub size_matched: Option<Decimal>,
571 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
572 pub size_remaining: Option<Decimal>,
573 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
574 pub size_lapsed: Option<Decimal>,
575 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
576 pub size_cancelled: Option<Decimal>,
577 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
578 pub size_voided: Option<Decimal>,
579 pub regulator_auth_code: Option<String>,
580 pub regulator_code: Option<String>,
581 pub customer_order_ref: Option<CustomerOrderRef>,
582 pub customer_strategy_ref: Option<CustomerStrategyRef>,
583}
584
585#[derive(Debug, Clone, Deserialize)]
587#[serde(rename_all = "camelCase")]
588pub struct CurrentOrderSummaryReport {
589 pub current_orders: Vec<CurrentOrderSummary>,
590 pub more_available: bool,
591}
592
593#[derive(Debug, Clone, Deserialize)]
595#[serde(rename_all = "camelCase")]
596pub struct ItemDescription {
597 pub event_type_desc: Option<String>,
598 pub event_desc: Option<String>,
599 pub market_desc: Option<String>,
600 pub market_type: Option<Ustr>,
601 pub market_start_time: Option<String>,
602 pub runner_desc: Option<String>,
603 pub number_of_winners: Option<u32>,
604 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
605 pub each_way_divisor: Option<Decimal>,
606}
607
608#[derive(Debug, Clone, Deserialize)]
610#[serde(rename_all = "camelCase")]
611pub struct ClearedOrderSummary {
612 pub event_type_id: Option<EventTypeId>,
613 pub event_id: Option<EventId>,
614 pub market_id: Option<MarketId>,
615 pub selection_id: Option<SelectionId>,
616 pub handicap: Option<Handicap>,
617 pub bet_id: Option<BetId>,
618 pub placed_date: Option<String>,
619 pub persistence_type: Option<PersistenceType>,
620 pub order_type: Option<BetfairOrderType>,
621 pub side: Option<BetfairSide>,
622 pub item_description: Option<ItemDescription>,
623 pub bet_outcome: Option<String>,
624 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
625 pub price_requested: Option<Decimal>,
626 pub settled_date: Option<String>,
627 pub last_matched_date: Option<String>,
628 pub bet_count: Option<u32>,
629 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
630 pub commission: Option<Decimal>,
631 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
632 pub price_matched: Option<Decimal>,
633 pub price_reduced: Option<bool>,
634 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
635 pub size_settled: Option<Decimal>,
636 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
637 pub profit: Option<Decimal>,
638 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
639 pub size_cancelled: Option<Decimal>,
640 pub customer_order_ref: Option<CustomerOrderRef>,
641 pub customer_strategy_ref: Option<CustomerStrategyRef>,
642}
643
644#[derive(Debug, Clone, Deserialize)]
646#[serde(rename_all = "camelCase")]
647pub struct ClearedOrderSummaryReport {
648 pub cleared_orders: Vec<ClearedOrderSummary>,
649 pub more_available: bool,
650}
651
652#[derive(Debug, Clone, Deserialize)]
654#[serde(rename_all = "camelCase")]
655pub struct NavigationMarket {
656 pub name: Option<String>,
657 pub id: Option<MarketId>,
658 pub exchange_id: Option<String>,
659 pub market_type: Option<Ustr>,
660 pub market_start_time: Option<String>,
661 #[serde(default, deserialize_with = "deserialize_optional_u32_lenient")]
662 pub number_of_winners: Option<u32>,
663}
664
665#[derive(Debug, Clone, Deserialize)]
667#[serde(rename_all = "camelCase")]
668pub struct NavigationRace {
669 pub name: Option<String>,
670 pub id: Option<String>,
671 pub venue: Option<Ustr>,
672 pub start_time: Option<String>,
673 pub race_number: Option<String>,
674 pub country_code: Option<Ustr>,
675 pub children: Option<Vec<NavigationChild>>,
676}
677
678#[derive(Debug, Clone, Deserialize)]
680#[serde(rename_all = "camelCase")]
681pub struct NavigationEvent {
682 pub name: Option<String>,
683 pub id: Option<EventId>,
684 pub country_code: Option<Ustr>,
685 pub children: Option<Vec<NavigationChild>>,
686}
687
688#[derive(Debug, Clone, Deserialize)]
690#[serde(rename_all = "camelCase")]
691pub struct NavigationGroup {
692 pub name: Option<String>,
693 pub id: Option<String>,
694 pub children: Option<Vec<NavigationChild>>,
695}
696
697#[derive(Debug, Clone, Deserialize)]
699#[serde(rename_all = "camelCase")]
700pub struct NavigationEventType {
701 pub name: Option<Ustr>,
702 pub id: Option<EventTypeId>,
703 pub children: Option<Vec<NavigationChild>>,
704}
705
706#[derive(Debug, Clone, Deserialize)]
708#[serde(tag = "type")]
709pub enum NavigationChild {
710 #[serde(rename = "EVENT_TYPE")]
711 EventType(NavigationEventType),
712 #[serde(rename = "GROUP")]
713 Group(NavigationGroup),
714 #[serde(rename = "EVENT")]
715 Event(NavigationEvent),
716 #[serde(rename = "RACE")]
717 Race(NavigationRace),
718 #[serde(rename = "MARKET")]
719 Market(NavigationMarket),
720}
721
722#[derive(Debug, Clone, Deserialize)]
724pub struct Navigation {
725 pub children: Option<Vec<NavigationChild>>,
726}
727
728#[derive(Debug, Clone)]
730pub struct FlattenedMarket {
731 pub event_type_id: Option<String>,
732 pub event_type_name: Option<Ustr>,
733 pub event_id: Option<String>,
734 pub event_name: Option<String>,
735 pub event_country_code: Option<Ustr>,
736 pub market_id: Option<MarketId>,
737 pub market_name: Option<String>,
738 pub market_type: Option<Ustr>,
739 pub market_start_time: Option<String>,
740 pub number_of_winners: Option<u32>,
741}
742
743#[cfg(test)]
744mod tests {
745 use nautilus_core::string::secret::REDACTED;
746 use rstest::rstest;
747
748 use super::*;
749 use crate::common::testing::{load_test_json, parse_jsonrpc};
750
751 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
752
753 #[rstest]
754 fn test_cert_login_response() {
755 let data = load_test_json("rest/cert_login.json");
756 let resp: CertLoginResponse = serde_json::from_str(&data).unwrap();
757 assert_eq!(resp.login_status, CertLoginStatus::Success);
758 assert!(resp.session_token.is_some());
759 }
760
761 #[rstest]
762 fn test_cert_login_error_response() {
763 let json = r#"{"loginStatus":"CERT_AUTH_REQUIRED"}"#;
764 let resp: CertLoginResponse = serde_json::from_str(json).unwrap();
765 assert_eq!(resp.login_status, CertLoginStatus::CertAuthRequired);
766 assert!(resp.session_token.is_none());
767 }
768
769 #[rstest]
770 fn test_cert_login_unknown_status_deserializes_to_other() {
771 let json = r#"{"loginStatus":"SOME_FUTURE_CODE"}"#;
772 let resp: CertLoginResponse = serde_json::from_str(json).unwrap();
773 assert_eq!(resp.login_status, CertLoginStatus::Other);
774 }
775
776 #[rstest]
777 fn test_interactive_login_response() {
778 let data = load_test_json("rest/login_success.json");
779 let resp: LoginResponse = serde_json::from_str(&data).unwrap();
780 assert_eq!(resp.status, LoginStatus::Success);
781 }
782
783 #[rstest]
784 fn test_login_responses_redact_tokens() {
785 assert_zeroize_on_drop::<LoginResponse>();
786 assert_zeroize_on_drop::<CertLoginResponse>();
787
788 let login = LoginResponse {
789 token: SecretString::from("interactive-session-token"),
790 product: "product".to_string(),
791 status: LoginStatus::Success,
792 error: None,
793 };
794 let certificate = CertLoginResponse {
795 session_token: Some(SecretString::from("certificate-session-token")),
796 login_status: CertLoginStatus::Success,
797 };
798
799 let formatted = format!("{login:?} {certificate:?}");
800
801 assert_eq!(formatted.matches(REDACTED).count(), 2);
802 assert!(!formatted.contains("interactive-session-token"));
803 assert!(!formatted.contains("certificate-session-token"));
804 }
805
806 #[rstest]
807 fn test_interactive_login_failure() {
808 let data = load_test_json("rest/login_failure.json");
809 let resp: LoginResponse = serde_json::from_str(&data).unwrap();
810 assert_eq!(resp.status, LoginStatus::Fail);
811 }
812
813 #[rstest]
814 fn test_list_market_catalogue_with_runner_metadata() {
815 let data = load_test_json("rest/list_market_catalogue.json");
816 let catalogue: MarketCatalogue = serde_json::from_str(&data).unwrap();
817 let runners = catalogue.runners.expect("runners present");
818 let meta = runners[0].metadata.as_ref().expect("metadata present");
819 assert!(meta.contains_key("AGE"));
820 assert!(meta.contains_key("CLOTH_NUMBER"));
821 assert!(meta.contains_key("STALL_DRAW"));
822 }
823
824 #[rstest]
825 fn test_navigation_with_empty_number_of_winners() {
826 let data = load_test_json("rest/navigation_list_navigation.json");
827 let nav: Navigation = serde_json::from_str(&data).unwrap();
828 assert!(nav.children.is_some());
829 }
830
831 fn find_race_with_children(children: &[NavigationChild]) -> bool {
832 for child in children {
833 match child {
834 NavigationChild::Race(race) => {
835 if let Some(kids) = &race.children
836 && !kids.is_empty()
837 {
838 return true;
839 }
840 }
841 NavigationChild::EventType(et) => {
842 if let Some(kids) = &et.children
843 && find_race_with_children(kids)
844 {
845 return true;
846 }
847 }
848 NavigationChild::Group(g) => {
849 if let Some(kids) = &g.children
850 && find_race_with_children(kids)
851 {
852 return true;
853 }
854 }
855 NavigationChild::Event(e) => {
856 if let Some(kids) = &e.children
857 && find_race_with_children(kids)
858 {
859 return true;
860 }
861 }
862 NavigationChild::Market(_) => {}
863 }
864 }
865 false
866 }
867
868 #[rstest]
869 fn test_navigation_race_has_children() {
870 let data = load_test_json("rest/navigation_list_navigation.json");
871 let nav: Navigation = serde_json::from_str(&data).unwrap();
872 let children = nav.children.as_ref().unwrap();
873 assert!(
874 find_race_with_children(children),
875 "should find at least one RACE node with MARKET children"
876 );
877 }
878
879 #[rstest]
880 fn test_account_details() {
881 let data = load_test_json("rest/account_details.json");
882 let _resp: AccountDetailsResponse = serde_json::from_str(&data).unwrap();
883 }
884
885 #[rstest]
886 #[case("rest/account_funds_no_exposure.json")]
887 #[case("rest/account_funds_with_exposure.json")]
888 fn test_account_funds(#[case] fixture: &str) {
889 let data = load_test_json(fixture);
890 let _resp: AccountFundsResponse =
891 serde_json::from_str(&data).unwrap_or_else(|e| panic!("{fixture}: {e}"));
892 }
893
894 #[rstest]
895 #[case("rest/betting_place_order_success.json")]
896 #[case("rest/betting_place_order_error.json")]
897 #[case("rest/betting_place_order_batch_success.json")]
898 #[case("rest/betting_place_order_batch_partial_failure.json")]
899 fn test_place_order_responses(#[case] fixture: &str) {
900 let data = load_test_json(fixture);
901 let _resp: PlaceExecutionReport = parse_jsonrpc(&data);
902 }
903
904 #[rstest]
905 #[case("rest/betting_cancel_orders_success.json")]
906 #[case("rest/betting_cancel_orders_error.json")]
907 #[case("rest/betting_cancel_orders_batch_success.json")]
908 #[case("rest/betting_cancel_orders_batch_partial_failure.json")]
909 fn test_cancel_order_responses(#[case] fixture: &str) {
910 let data = load_test_json(fixture);
911 let _resp: CancelExecutionReport = parse_jsonrpc(&data);
912 }
913
914 #[rstest]
915 #[case("rest/betting_replace_orders_success.json")]
916 #[case("rest/betting_replace_orders_cancelled_not_placed_live.json")]
917 fn test_replace_order_responses(#[case] fixture: &str) {
918 let data = load_test_json(fixture);
921 let _resp: ReplaceExecutionReport = parse_jsonrpc(&data);
922 }
923
924 #[rstest]
925 #[case("rest/list_current_orders_empty.json")]
926 #[case("rest/list_current_orders_single.json")]
927 #[case("rest/list_current_orders_executable.json")]
928 #[case("rest/list_current_orders_execution_complete.json")]
929 #[case("rest/list_current_orders_on_close_execution_complete.json")]
930 fn test_current_orders(#[case] fixture: &str) {
931 let data = load_test_json(fixture);
932 let _resp: CurrentOrderSummaryReport = parse_jsonrpc(&data);
933 }
934
935 #[rstest]
936 fn test_cleared_orders() {
937 let data = load_test_json("rest/list_cleared_orders.json");
938 let _resp: ClearedOrderSummaryReport = parse_jsonrpc(&data);
939 }
940
941 #[rstest]
942 fn test_betting_market_catalogue() {
943 let data = load_test_json("rest/betting_list_market_catalogue.json");
944 let catalogues: Vec<MarketCatalogue> = serde_json::from_str(&data).unwrap();
945 assert!(!catalogues.is_empty());
946 }
947}