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