1use crate::presentation::instrument::InstrumentType;
2use crate::presentation::serialization::{string_as_bool_opt, string_as_float_opt};
3use pretty_simple_display::{DebugPretty, DisplaySimple};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
9pub struct Instrument {
10 pub epic: String,
12 pub name: String,
14 pub expiry: String,
16 #[serde(rename = "contractSize")]
18 pub contract_size: String,
19 #[serde(rename = "lotSize")]
21 pub lot_size: Option<f64>,
22 #[serde(rename = "highLimitPrice")]
24 pub high_limit_price: Option<f64>,
25 #[serde(rename = "lowLimitPrice")]
27 pub low_limit_price: Option<f64>,
28 #[serde(rename = "marginFactor")]
30 pub margin_factor: Option<f64>,
31 #[serde(rename = "marginFactorUnit")]
33 pub margin_factor_unit: Option<String>,
34 pub currencies: Option<Vec<Currency>>,
36 #[serde(rename = "valueOfOnePip")]
37 pub value_of_one_pip: String,
39 #[serde(rename = "instrumentType")]
41 pub instrument_type: Option<InstrumentType>,
42 #[serde(rename = "expiryDetails")]
44 pub expiry_details: Option<ExpiryDetails>,
45 #[serde(rename = "slippageFactor")]
46 pub slippage_factor: Option<StepDistance>,
48 #[serde(rename = "limitedRiskPremium")]
49 pub limited_risk_premium: Option<StepDistance>,
51 #[serde(rename = "newsCode")]
52 pub news_code: Option<String>,
54 #[serde(rename = "chartCode")]
55 pub chart_code: Option<String>,
57}
58
59#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
61pub struct Currency {
62 pub code: String,
64 pub symbol: Option<String>,
66 #[serde(rename = "baseExchangeRate")]
68 pub base_exchange_rate: Option<f64>,
69 #[serde(rename = "exchangeRate")]
71 pub exchange_rate: Option<f64>,
72 #[serde(rename = "isDefault")]
74 pub is_default: Option<bool>,
75}
76
77#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
79pub struct MarketDetails {
80 pub instrument: Instrument,
82 pub snapshot: MarketSnapshot,
84 #[serde(rename = "dealingRules")]
86 pub dealing_rules: DealingRules,
87}
88
89#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
91pub struct DealingRules {
92 #[serde(rename = "minStepDistance")]
94 pub min_step_distance: Option<StepDistance>,
95
96 #[serde(rename = "minDealSize")]
98 pub min_deal_size: Option<StepDistance>,
99
100 #[serde(rename = "minControlledRiskStopDistance")]
102 pub min_controlled_risk_stop_distance: Option<StepDistance>,
103
104 #[serde(rename = "minNormalStopOrLimitDistance")]
106 pub min_normal_stop_or_limit_distance: Option<StepDistance>,
107
108 #[serde(rename = "maxStopOrLimitDistance")]
110 pub max_stop_or_limit_distance: Option<StepDistance>,
111
112 #[serde(rename = "controlledRiskSpacing")]
114 pub controlled_risk_spacing: Option<StepDistance>,
115
116 #[serde(rename = "marketOrderPreference")]
118 pub market_order_preference: String,
119
120 #[serde(rename = "trailingStopsPreference")]
122 pub trailing_stops_preference: String,
123
124 #[serde(rename = "maxDealSize")]
125 pub max_deal_size: Option<f64>,
127}
128
129#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
131pub struct MarketSnapshot {
132 #[serde(rename = "marketStatus")]
134 pub market_status: String,
135
136 #[serde(rename = "netChange")]
138 pub net_change: Option<f64>,
139
140 #[serde(rename = "percentageChange")]
142 pub percentage_change: Option<f64>,
143
144 #[serde(rename = "updateTime")]
146 pub update_time: Option<String>,
147
148 #[serde(rename = "delayTime")]
150 pub delay_time: Option<i64>,
151
152 pub bid: Option<f64>,
154
155 pub offer: Option<f64>,
157
158 pub high: Option<f64>,
160
161 pub low: Option<f64>,
163
164 #[serde(rename = "binaryOdds")]
166 pub binary_odds: Option<f64>,
167
168 #[serde(rename = "decimalPlacesFactor")]
170 pub decimal_places_factor: Option<i64>,
171
172 #[serde(rename = "scalingFactor")]
174 pub scaling_factor: Option<i64>,
175
176 #[serde(rename = "controlledRiskExtraSpread")]
178 pub controlled_risk_extra_spread: Option<f64>,
179}
180
181#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
183pub struct MarketData {
184 pub epic: String,
186 #[serde(rename = "instrumentName")]
188 pub instrument_name: String,
189 #[serde(rename = "instrumentType")]
191 pub instrument_type: InstrumentType,
192 pub expiry: String,
194 #[serde(rename = "highLimitPrice")]
196 pub high_limit_price: Option<f64>,
197 #[serde(rename = "lowLimitPrice")]
199 pub low_limit_price: Option<f64>,
200 #[serde(rename = "marketStatus")]
202 pub market_status: String,
203 #[serde(rename = "netChange")]
205 pub net_change: Option<f64>,
206 #[serde(rename = "percentageChange")]
208 pub percentage_change: Option<f64>,
209 #[serde(rename = "updateTime")]
211 pub update_time: Option<String>,
212 #[serde(rename = "updateTimeUTC")]
214 pub update_time_utc: Option<String>,
215 pub bid: Option<f64>,
217 pub offer: Option<f64>,
219}
220
221impl MarketData {
222 #[must_use]
235 #[inline]
236 pub fn is_call(&self) -> bool {
237 self.instrument_name.contains("CALL")
238 }
239
240 #[must_use]
252 #[inline]
253 pub fn is_put(&self) -> bool {
254 self.instrument_name.contains("PUT")
255 }
256}
257
258#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
260pub struct HistoricalPrice {
261 #[serde(rename = "snapshotTime")]
266 pub snapshot_time: String,
267 #[serde(rename = "snapshotTimeUTC", default)]
272 pub snapshot_time_utc: Option<String>,
273 #[serde(rename = "openPrice")]
275 pub open_price: PricePoint,
276 #[serde(rename = "highPrice")]
278 pub high_price: PricePoint,
279 #[serde(rename = "lowPrice")]
281 pub low_price: PricePoint,
282 #[serde(rename = "closePrice")]
284 pub close_price: PricePoint,
285 #[serde(rename = "lastTradedVolume")]
287 pub last_traded_volume: Option<i64>,
288}
289
290#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
292pub struct PricePoint {
293 pub bid: Option<f64>,
295 pub ask: Option<f64>,
297 #[serde(rename = "lastTraded")]
299 pub last_traded: Option<f64>,
300}
301
302#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
304pub struct PriceAllowance {
305 #[serde(rename = "remainingAllowance")]
307 pub remaining_allowance: i64,
308 #[serde(rename = "totalAllowance")]
310 pub total_allowance: i64,
311 #[serde(rename = "allowanceExpiry")]
313 pub allowance_expiry: i64,
314}
315
316#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
318pub struct ExpiryDetails {
319 #[serde(rename = "lastDealingDate")]
321 pub last_dealing_date: String,
322
323 #[serde(rename = "settlementInfo")]
325 pub settlement_info: Option<String>,
326}
327
328#[repr(u8)]
329#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
330pub enum StepUnit {
332 #[serde(rename = "POINTS")]
333 Points,
335 #[serde(rename = "PERCENTAGE")]
336 Percentage,
338 #[serde(rename = "pct")]
339 Pct,
341}
342
343#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
345pub struct StepDistance {
346 pub unit: Option<StepUnit>,
348 pub value: Option<f64>,
350}
351
352#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
354pub struct MarketNavigationNode {
355 pub id: String,
357 pub name: String,
359}
360
361#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
363pub struct MarketNode {
364 pub id: String,
366 pub name: String,
368 #[serde(skip_serializing_if = "Vec::is_empty", default)]
370 pub children: Vec<MarketNode>,
371 #[serde(skip_serializing_if = "Vec::is_empty", default)]
373 pub markets: Vec<MarketData>,
374}
375
376#[repr(u8)]
378#[derive(
379 DebugPretty, DisplaySimple, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Default,
380)]
381#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
382pub enum MarketState {
383 Closed,
385 #[default]
387 Offline,
388 Tradeable,
390 Edit,
392 EditsOnly,
394 Auction,
396 AuctionNoEdit,
398 Suspended,
400 OnAuction,
402 OnAuctionNoEdits,
404}
405
406#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
408pub struct PresentationMarketData {
409 pub item_name: String,
411 pub item_pos: usize,
413 pub fields: MarketFields,
415 pub changed_fields: MarketFields,
417 pub is_snapshot: bool,
419}
420
421impl PresentationMarketData {
422 pub fn from_fields(
439 item_name: Option<&str>,
440 item_pos: usize,
441 is_snapshot: bool,
442 fields: &HashMap<String, Option<String>>,
443 changed_fields: &HashMap<String, Option<String>>,
444 ) -> Result<Self, String> {
445 let fields = Self::create_market_fields(fields)?;
446 let changed_fields = Self::create_market_fields(changed_fields)?;
447
448 Ok(PresentationMarketData {
449 item_name: item_name.unwrap_or_default().to_string(),
450 item_pos,
451 fields,
452 changed_fields,
453 is_snapshot,
454 })
455 }
456
457 fn create_market_fields(
465 fields_map: &HashMap<String, Option<String>>,
466 ) -> Result<MarketFields, String> {
467 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
469
470 let market_state = match get_field("MARKET_STATE").as_deref() {
472 Some("closed") => Some(MarketState::Closed),
473 Some("offline") => Some(MarketState::Offline),
474 Some("tradeable") => Some(MarketState::Tradeable),
475 Some("edit") => Some(MarketState::Edit),
476 Some("auction") => Some(MarketState::Auction),
477 Some("auction_no_edit") => Some(MarketState::AuctionNoEdit),
478 Some("suspended") => Some(MarketState::Suspended),
479 Some("on_auction") => Some(MarketState::OnAuction),
480 Some("on_auction_no_edit") => Some(MarketState::OnAuctionNoEdits),
481 Some(unknown) => return Err(format!("Unknown market state: {unknown}")),
482 None => None,
483 };
484
485 let market_delay = match get_field("MARKET_DELAY").as_deref() {
487 Some("0") => Some(false),
488 Some("1") => Some(true),
489 Some(val) => return Err(format!("Invalid MARKET_DELAY value: {val}")),
490 None => None,
491 };
492
493 let parse_float = |key: &str| -> Result<Option<f64>, String> {
495 match get_field(key) {
496 Some(val) if !val.is_empty() => val
497 .parse::<f64>()
498 .map(Some)
499 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
500 _ => Ok(None),
501 }
502 };
503
504 Ok(MarketFields {
505 mid_open: parse_float("MID_OPEN")?,
506 high: parse_float("HIGH")?,
507 offer: parse_float("OFFER")?,
508 change: parse_float("CHANGE")?,
509 market_delay,
510 low: parse_float("LOW")?,
511 bid: parse_float("BID")?,
512 change_pct: parse_float("CHANGE_PCT")?,
513 market_state,
514 update_time: get_field("UPDATE_TIME"),
515 })
516 }
517}
518
519#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
521pub struct Category {
522 pub code: String,
524 #[serde(rename = "nonTradeable")]
526 pub non_tradeable: bool,
527}
528
529#[repr(u8)]
531#[derive(
532 DebugPretty, DisplaySimple, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash,
533)]
534#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
535pub enum CategoryMarketStatus {
536 #[default]
538 Offline,
539 Closed,
541 Suspended,
543 OnAuction,
545 OnAuctionNoEdits,
547 EditsOnly,
549 ClosingsOnly,
551 DealNoEdit,
553 Tradeable,
555}
556
557#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
559pub struct CategoryInstrument {
560 pub epic: String,
562 #[serde(rename = "instrumentName")]
564 pub instrument_name: String,
565 pub expiry: String,
567 #[serde(rename = "instrumentType")]
569 pub instrument_type: InstrumentType,
570 #[serde(rename = "lotSize")]
572 pub lot_size: Option<f64>,
573 #[serde(rename = "otcTradeable")]
575 pub otc_tradeable: bool,
576 #[serde(rename = "marketStatus")]
578 pub market_status: CategoryMarketStatus,
579 #[serde(rename = "delayTime")]
581 pub delay_time: Option<i64>,
582 pub bid: Option<f64>,
584 pub offer: Option<f64>,
586 pub high: Option<f64>,
588 pub low: Option<f64>,
590 #[serde(rename = "netChange")]
592 pub net_change: Option<f64>,
593 #[serde(rename = "percentageChange")]
595 pub percentage_change: Option<f64>,
596 #[serde(rename = "updateTime")]
598 pub update_time: Option<String>,
599 #[serde(rename = "scalingFactor")]
601 pub scaling_factor: Option<i64>,
602 #[serde(rename = "expiryTimestamp")]
609 pub expiry_timestamp: Option<i64>,
610 #[serde(rename = "underlyingName")]
612 pub underlying_name: Option<String>,
613 pub popularity: Option<i64>,
615 #[serde(rename = "marketType")]
617 pub market_type: Option<String>,
618 #[serde(rename = "marketSubtype")]
620 pub market_subtype: Option<String>,
621}
622
623#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
625pub struct CategoryInstrumentsMetadata {
626 #[serde(rename = "pageNumber")]
628 pub page_number: i64,
629 #[serde(rename = "pageSize")]
631 pub page_size: i64,
632}
633
634#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
636pub struct MarketFields {
637 #[serde(rename = "MID_OPEN")]
639 #[serde(with = "string_as_float_opt")]
640 #[serde(default)]
641 pub mid_open: Option<f64>,
642
643 #[serde(rename = "HIGH")]
645 #[serde(with = "string_as_float_opt")]
646 #[serde(default)]
647 pub high: Option<f64>,
648
649 #[serde(rename = "OFFER")]
651 #[serde(with = "string_as_float_opt")]
652 #[serde(default)]
653 pub offer: Option<f64>,
654
655 #[serde(rename = "CHANGE")]
657 #[serde(with = "string_as_float_opt")]
658 #[serde(default)]
659 pub change: Option<f64>,
660
661 #[serde(rename = "MARKET_DELAY")]
663 #[serde(with = "string_as_bool_opt")]
664 #[serde(default)]
665 pub market_delay: Option<bool>,
666
667 #[serde(rename = "LOW")]
669 #[serde(with = "string_as_float_opt")]
670 #[serde(default)]
671 pub low: Option<f64>,
672
673 #[serde(rename = "BID")]
675 #[serde(with = "string_as_float_opt")]
676 #[serde(default)]
677 pub bid: Option<f64>,
678
679 #[serde(rename = "CHANGE_PCT")]
681 #[serde(with = "string_as_float_opt")]
682 #[serde(default)]
683 pub change_pct: Option<f64>,
684
685 #[serde(rename = "MARKET_STATE")]
687 #[serde(default)]
688 pub market_state: Option<MarketState>,
689
690 #[serde(rename = "UPDATE_TIME")]
692 #[serde(default)]
693 pub update_time: Option<String>,
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 #[test]
701 fn test_historical_price_captures_snapshot_time_utc() {
702 let json = r#"{
704 "snapshotTime": "2024/01/15 15:30:00",
705 "snapshotTimeUTC": "2024-01-15T14:30:00",
706 "openPrice": { "bid": 1.1, "ask": 1.2, "lastTraded": null },
707 "highPrice": { "bid": 1.3, "ask": 1.4, "lastTraded": null },
708 "lowPrice": { "bid": 1.0, "ask": 1.05, "lastTraded": null },
709 "closePrice": { "bid": 1.25, "ask": 1.26, "lastTraded": null },
710 "lastTradedVolume": 42
711 }"#;
712 let hp: HistoricalPrice = serde_json::from_str(json).expect("deserialize");
713 assert_eq!(hp.snapshot_time, "2024/01/15 15:30:00");
714 assert_eq!(hp.snapshot_time_utc.as_deref(), Some("2024-01-15T14:30:00"));
715
716 let round: HistoricalPrice =
718 serde_json::from_str(&serde_json::to_string(&hp).expect("ser")).expect("de");
719 assert_eq!(
720 round.snapshot_time_utc.as_deref(),
721 Some("2024-01-15T14:30:00")
722 );
723
724 let legacy = r#"{
726 "snapshotTime": "2024/01/15 15:30:00",
727 "openPrice": { "bid": 1.1, "ask": 1.2, "lastTraded": null },
728 "highPrice": { "bid": 1.3, "ask": 1.4, "lastTraded": null },
729 "lowPrice": { "bid": 1.0, "ask": 1.05, "lastTraded": null },
730 "closePrice": { "bid": 1.25, "ask": 1.26, "lastTraded": null },
731 "lastTradedVolume": null
732 }"#;
733 let hp2: HistoricalPrice = serde_json::from_str(legacy).expect("deserialize legacy");
734 assert!(hp2.snapshot_time_utc.is_none());
735 }
736
737 #[test]
738 fn test_market_data_is_call_returns_true_for_call_option() {
739 let market = MarketData {
740 epic: "test".to_string(),
741 instrument_name: "DAX CALL 18000".to_string(),
742 instrument_type: InstrumentType::default(),
743 expiry: "-".to_string(),
744 high_limit_price: None,
745 low_limit_price: None,
746 market_status: "TRADEABLE".to_string(),
747 net_change: None,
748 percentage_change: None,
749 update_time: None,
750 update_time_utc: None,
751 bid: Some(100.0),
752 offer: Some(101.0),
753 };
754 assert!(market.is_call());
755 assert!(!market.is_put());
756 }
757
758 #[test]
759 fn test_market_data_is_put_returns_true_for_put_option() {
760 let market = MarketData {
761 epic: "test".to_string(),
762 instrument_name: "DAX PUT 17000".to_string(),
763 instrument_type: InstrumentType::default(),
764 expiry: "-".to_string(),
765 high_limit_price: None,
766 low_limit_price: None,
767 market_status: "TRADEABLE".to_string(),
768 net_change: None,
769 percentage_change: None,
770 update_time: None,
771 update_time_utc: None,
772 bid: Some(50.0),
773 offer: Some(51.0),
774 };
775 assert!(market.is_put());
776 assert!(!market.is_call());
777 }
778
779 #[test]
780 fn test_market_data_neither_call_nor_put() {
781 let market = MarketData {
782 epic: "IX.D.DAX.DAILY.IP".to_string(),
783 instrument_name: "Germany 40".to_string(),
784 instrument_type: InstrumentType::default(),
785 expiry: "-".to_string(),
786 high_limit_price: None,
787 low_limit_price: None,
788 market_status: "TRADEABLE".to_string(),
789 net_change: None,
790 percentage_change: None,
791 update_time: None,
792 update_time_utc: None,
793 bid: Some(18000.0),
794 offer: Some(18001.0),
795 };
796 assert!(!market.is_call());
797 assert!(!market.is_put());
798 }
799
800 #[test]
801 fn test_category_instruments_metadata_deserialize_and_roundtrip() {
802 let json = r#"{ "pageNumber": 2, "pageSize": 1000 }"#;
805
806 let meta: CategoryInstrumentsMetadata =
807 serde_json::from_str(json).expect("deserialize failed");
808 assert_eq!(meta.page_number, 2);
809 assert_eq!(meta.page_size, 1000);
810
811 let serialized = serde_json::to_string(&meta).expect("serialize failed");
812 let re: CategoryInstrumentsMetadata =
813 serde_json::from_str(&serialized).expect("re-deserialize failed");
814 assert_eq!(re, meta);
815 assert!(serialized.contains("\"pageNumber\":2"));
816 assert!(serialized.contains("\"pageSize\":1000"));
817 }
818
819 #[test]
820 fn test_market_state_default() {
821 let state = MarketState::default();
822 assert_eq!(state, MarketState::Offline);
823 }
824
825 #[test]
826 fn test_category_market_status_default() {
827 let status = CategoryMarketStatus::default();
828 assert_eq!(status, CategoryMarketStatus::Offline);
829 }
830
831 #[test]
832 fn test_step_unit_serialization() {
833 let points = StepUnit::Points;
834 let json = serde_json::to_string(&points).expect("serialize failed");
835 assert_eq!(json, "\"POINTS\"");
836
837 let pct = StepUnit::Percentage;
838 let json = serde_json::to_string(&pct).expect("serialize failed");
839 assert_eq!(json, "\"PERCENTAGE\"");
840 }
841
842 #[test]
843 fn test_step_distance_creation() {
844 let distance = StepDistance {
845 unit: Some(StepUnit::Points),
846 value: Some(1.5),
847 };
848 assert_eq!(distance.unit, Some(StepUnit::Points));
849 assert_eq!(distance.value, Some(1.5));
850 }
851
852 #[test]
853 fn test_market_fields_default() {
854 let fields = MarketFields::default();
855 assert!(fields.mid_open.is_none());
856 assert!(fields.high.is_none());
857 assert!(fields.offer.is_none());
858 assert!(fields.change.is_none());
859 assert!(fields.market_delay.is_none());
860 assert!(fields.low.is_none());
861 assert!(fields.bid.is_none());
862 assert!(fields.change_pct.is_none());
863 assert!(fields.market_state.is_none());
864 assert!(fields.update_time.is_none());
865 }
866
867 #[test]
868 fn test_presentation_market_data_default() {
869 let data = PresentationMarketData::default();
870 assert!(data.item_name.is_empty());
871 assert_eq!(data.item_pos, 0);
872 assert!(!data.is_snapshot);
873 }
874
875 #[test]
876 fn test_category_default() {
877 let cat = Category::default();
878 assert!(cat.code.is_empty());
879 assert!(!cat.non_tradeable);
880 }
881
882 #[test]
883 fn test_category_instrument_default() {
884 let inst = CategoryInstrument::default();
885 assert!(inst.epic.is_empty());
886 assert!(inst.instrument_name.is_empty());
887 assert_eq!(inst.market_status, CategoryMarketStatus::Offline);
888 assert!(inst.expiry_timestamp.is_none());
889 assert!(inst.underlying_name.is_none());
890 assert!(inst.popularity.is_none());
891 assert!(inst.market_type.is_none());
892 assert!(inst.market_subtype.is_none());
893 }
894
895 #[test]
896 fn test_category_instrument_deserialization_full_payload() {
897 let json = r#"{
898 "epic": "OD.D.OTCWK2AUD.28.IP",
899 "instrumentName": "Weekly AUDUSD ($1) 7200 Call",
900 "expiry": "17-JUL-26",
901 "instrumentType": "OPT_CURRENCIES",
902 "lotSize": 1.0,
903 "otcTradeable": true,
904 "marketStatus": "EDITS_ONLY",
905 "delayTime": 0,
906 "bid": 0.0,
907 "offer": 4.0,
908 "high": 4.0,
909 "low": 0.0,
910 "netChange": 6936.2,
911 "percentageChange": 0.0,
912 "updateTime": "06:53:51",
913 "scalingFactor": 1,
914 "underlyingName": "AUD/USD",
915 "popularity": 91783089620,
916 "marketType": "FX_PAIR",
917 "marketSubtype": "Fiat",
918 "expiryTimestamp": 1784300400000
919 }"#;
920
921 let inst: CategoryInstrument = serde_json::from_str(json).expect("deserialize failed");
922 assert_eq!(inst.epic, "OD.D.OTCWK2AUD.28.IP");
923 assert_eq!(inst.instrument_name, "Weekly AUDUSD ($1) 7200 Call");
924 assert_eq!(inst.expiry, "17-JUL-26");
925 assert_eq!(inst.market_status, CategoryMarketStatus::EditsOnly);
926 assert_eq!(inst.expiry_timestamp, Some(1784300400000));
927 assert_eq!(inst.underlying_name.as_deref(), Some("AUD/USD"));
928 assert_eq!(inst.popularity, Some(91783089620));
929 assert_eq!(inst.market_type.as_deref(), Some("FX_PAIR"));
930 assert_eq!(inst.market_subtype.as_deref(), Some("Fiat"));
931 }
932
933 #[test]
934 fn test_category_instrument_deserialization_without_optional_fields() {
935 let json = r#"{
936 "epic": "IX.D.FTSE.DAILY.IP",
937 "instrumentName": "FTSE 100",
938 "expiry": "-",
939 "instrumentType": "INDICES",
940 "otcTradeable": false,
941 "marketStatus": "TRADEABLE"
942 }"#;
943
944 let inst: CategoryInstrument = serde_json::from_str(json).expect("deserialize failed");
945 assert_eq!(inst.epic, "IX.D.FTSE.DAILY.IP");
946 assert!(inst.expiry_timestamp.is_none());
947 assert!(inst.underlying_name.is_none());
948 assert!(inst.popularity.is_none());
949 assert!(inst.market_type.is_none());
950 assert!(inst.market_subtype.is_none());
951 }
952
953 #[test]
954 fn test_market_state_serialization() {
955 let tradeable = MarketState::Tradeable;
956 let json = serde_json::to_string(&tradeable).expect("serialize failed");
957 assert_eq!(json, "\"TRADEABLE\"");
958
959 let closed = MarketState::Closed;
960 let json = serde_json::to_string(&closed).expect("serialize failed");
961 assert_eq!(json, "\"CLOSED\"");
962 }
963
964 #[test]
965 fn test_price_point_creation() {
966 let point = PricePoint {
967 bid: Some(100.5),
968 ask: Some(101.0),
969 last_traded: Some(100.75),
970 };
971 assert_eq!(point.bid, Some(100.5));
972 assert_eq!(point.ask, Some(101.0));
973 assert_eq!(point.last_traded, Some(100.75));
974 }
975
976 #[test]
977 fn test_price_allowance_creation() {
978 let allowance = PriceAllowance {
979 remaining_allowance: 100,
980 total_allowance: 1000,
981 allowance_expiry: 3600,
982 };
983 assert_eq!(allowance.remaining_allowance, 100);
984 assert_eq!(allowance.total_allowance, 1000);
985 assert_eq!(allowance.allowance_expiry, 3600);
986 }
987
988 #[test]
989 fn test_expiry_details_creation() {
990 let expiry = ExpiryDetails {
991 last_dealing_date: "2024-12-31".to_string(),
992 settlement_info: Some("Cash settlement".to_string()),
993 };
994 assert_eq!(expiry.last_dealing_date, "2024-12-31");
995 assert_eq!(expiry.settlement_info, Some("Cash settlement".to_string()));
996 }
997
998 #[test]
999 fn test_market_navigation_node_creation() {
1000 let node = MarketNavigationNode {
1001 id: "12345".to_string(),
1002 name: "Indices".to_string(),
1003 };
1004 assert_eq!(node.id, "12345");
1005 assert_eq!(node.name, "Indices");
1006 }
1007
1008 #[test]
1009 fn test_market_node_creation() {
1010 let node = MarketNode {
1011 id: "node1".to_string(),
1012 name: "Test Node".to_string(),
1013 children: Vec::new(),
1014 markets: Vec::new(),
1015 };
1016 assert_eq!(node.id, "node1");
1017 assert_eq!(node.name, "Test Node");
1018 assert!(node.children.is_empty());
1019 assert!(node.markets.is_empty());
1020 }
1021
1022 #[test]
1023 fn test_currency_creation() {
1024 let currency = Currency {
1025 code: "USD".to_string(),
1026 symbol: Some("$".to_string()),
1027 base_exchange_rate: Some(1.0),
1028 exchange_rate: Some(1.0),
1029 is_default: Some(true),
1030 };
1031 assert_eq!(currency.code, "USD");
1032 assert_eq!(currency.symbol, Some("$".to_string()));
1033 assert_eq!(currency.is_default, Some(true));
1034 }
1035
1036 #[test]
1037 fn test_create_market_fields_with_valid_data() {
1038 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1039 fields_map.insert("BID".to_string(), Some("100.5".to_string()));
1040 fields_map.insert("OFFER".to_string(), Some("101.0".to_string()));
1041 fields_map.insert("HIGH".to_string(), Some("102.0".to_string()));
1042 fields_map.insert("LOW".to_string(), Some("99.0".to_string()));
1043 fields_map.insert("CHANGE".to_string(), Some("1.5".to_string()));
1044 fields_map.insert("CHANGE_PCT".to_string(), Some("1.5".to_string()));
1045 fields_map.insert("MARKET_STATE".to_string(), Some("tradeable".to_string()));
1046 fields_map.insert("MARKET_DELAY".to_string(), Some("0".to_string()));
1047 fields_map.insert("UPDATE_TIME".to_string(), Some("12:30:00".to_string()));
1048
1049 let result = PresentationMarketData::create_market_fields(&fields_map);
1050 assert!(result.is_ok());
1051
1052 let fields = result.expect("should parse");
1053 assert_eq!(fields.bid, Some(100.5));
1054 assert_eq!(fields.offer, Some(101.0));
1055 assert_eq!(fields.high, Some(102.0));
1056 assert_eq!(fields.low, Some(99.0));
1057 assert_eq!(fields.change, Some(1.5));
1058 assert_eq!(fields.market_state, Some(MarketState::Tradeable));
1059 assert_eq!(fields.market_delay, Some(false));
1060 assert_eq!(fields.update_time, Some("12:30:00".to_string()));
1061 }
1062
1063 #[test]
1064 fn test_create_market_fields_with_empty_map() {
1065 let fields_map: HashMap<String, Option<String>> = HashMap::new();
1066 let result = PresentationMarketData::create_market_fields(&fields_map);
1067 assert!(result.is_ok());
1068
1069 let fields = result.expect("should parse");
1070 assert!(fields.bid.is_none());
1071 assert!(fields.offer.is_none());
1072 }
1073
1074 #[test]
1075 fn test_create_market_fields_invalid_market_state() {
1076 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1077 fields_map.insert(
1078 "MARKET_STATE".to_string(),
1079 Some("invalid_state".to_string()),
1080 );
1081
1082 let result = PresentationMarketData::create_market_fields(&fields_map);
1083 assert!(result.is_err());
1084 }
1085
1086 #[test]
1087 fn test_create_market_fields_invalid_market_delay() {
1088 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1089 fields_map.insert("MARKET_DELAY".to_string(), Some("invalid".to_string()));
1090
1091 let result = PresentationMarketData::create_market_fields(&fields_map);
1092 assert!(result.is_err());
1093 }
1094
1095 #[test]
1096 fn test_create_market_fields_all_market_states() {
1097 let states = vec![
1098 ("closed", MarketState::Closed),
1099 ("offline", MarketState::Offline),
1100 ("tradeable", MarketState::Tradeable),
1101 ("edit", MarketState::Edit),
1102 ("auction", MarketState::Auction),
1103 ("auction_no_edit", MarketState::AuctionNoEdit),
1104 ("suspended", MarketState::Suspended),
1105 ("on_auction", MarketState::OnAuction),
1106 ("on_auction_no_edit", MarketState::OnAuctionNoEdits),
1107 ];
1108
1109 for (state_str, expected_state) in states {
1110 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1111 fields_map.insert("MARKET_STATE".to_string(), Some(state_str.to_string()));
1112
1113 let result = PresentationMarketData::create_market_fields(&fields_map);
1114 assert!(result.is_ok(), "Failed for state: {}", state_str);
1115 let fields = result.expect("should parse");
1116 assert_eq!(fields.market_state, Some(expected_state));
1117 }
1118 }
1119
1120 #[test]
1121 fn test_market_delay_values() {
1122 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1123 fields_map.insert("MARKET_DELAY".to_string(), Some("1".to_string()));
1124
1125 let result = PresentationMarketData::create_market_fields(&fields_map);
1126 assert!(result.is_ok());
1127 let fields = result.expect("should parse");
1128 assert_eq!(fields.market_delay, Some(true));
1129 }
1130}