1use crate::presentation::instrument::InstrumentType;
2use crate::presentation::serialization::{string_as_bool_opt, string_as_float_opt};
3use lightstreamer_rs::subscription::ItemUpdate;
4use pretty_simple_display::{DebugPretty, DisplaySimple};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
10pub struct Instrument {
11 pub epic: String,
13 pub name: String,
15 pub expiry: String,
17 #[serde(rename = "contractSize")]
19 pub contract_size: String,
20 #[serde(rename = "lotSize")]
22 pub lot_size: Option<f64>,
23 #[serde(rename = "highLimitPrice")]
25 pub high_limit_price: Option<f64>,
26 #[serde(rename = "lowLimitPrice")]
28 pub low_limit_price: Option<f64>,
29 #[serde(rename = "marginFactor")]
31 pub margin_factor: Option<f64>,
32 #[serde(rename = "marginFactorUnit")]
34 pub margin_factor_unit: Option<String>,
35 pub currencies: Option<Vec<Currency>>,
37 #[serde(rename = "valueOfOnePip")]
38 pub value_of_one_pip: String,
40 #[serde(rename = "instrumentType")]
42 pub instrument_type: Option<InstrumentType>,
43 #[serde(rename = "expiryDetails")]
45 pub expiry_details: Option<ExpiryDetails>,
46 #[serde(rename = "slippageFactor")]
47 pub slippage_factor: Option<StepDistance>,
49 #[serde(rename = "limitedRiskPremium")]
50 pub limited_risk_premium: Option<StepDistance>,
52 #[serde(rename = "newsCode")]
53 pub news_code: Option<String>,
55 #[serde(rename = "chartCode")]
56 pub chart_code: Option<String>,
58}
59
60#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
62pub struct Currency {
63 pub code: String,
65 pub symbol: Option<String>,
67 #[serde(rename = "baseExchangeRate")]
69 pub base_exchange_rate: Option<f64>,
70 #[serde(rename = "exchangeRate")]
72 pub exchange_rate: Option<f64>,
73 #[serde(rename = "isDefault")]
75 pub is_default: Option<bool>,
76}
77
78#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
80pub struct MarketDetails {
81 pub instrument: Instrument,
83 pub snapshot: MarketSnapshot,
85 #[serde(rename = "dealingRules")]
87 pub dealing_rules: DealingRules,
88}
89
90#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
92pub struct DealingRules {
93 #[serde(rename = "minStepDistance")]
95 pub min_step_distance: Option<StepDistance>,
96
97 #[serde(rename = "minDealSize")]
99 pub min_deal_size: Option<StepDistance>,
100
101 #[serde(rename = "minControlledRiskStopDistance")]
103 pub min_controlled_risk_stop_distance: Option<StepDistance>,
104
105 #[serde(rename = "minNormalStopOrLimitDistance")]
107 pub min_normal_stop_or_limit_distance: Option<StepDistance>,
108
109 #[serde(rename = "maxStopOrLimitDistance")]
111 pub max_stop_or_limit_distance: Option<StepDistance>,
112
113 #[serde(rename = "controlledRiskSpacing")]
115 pub controlled_risk_spacing: Option<StepDistance>,
116
117 #[serde(rename = "marketOrderPreference")]
119 pub market_order_preference: String,
120
121 #[serde(rename = "trailingStopsPreference")]
123 pub trailing_stops_preference: String,
124
125 #[serde(rename = "maxDealSize")]
126 pub max_deal_size: Option<f64>,
128}
129
130#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
132pub struct MarketSnapshot {
133 #[serde(rename = "marketStatus")]
135 pub market_status: String,
136
137 #[serde(rename = "netChange")]
139 pub net_change: Option<f64>,
140
141 #[serde(rename = "percentageChange")]
143 pub percentage_change: Option<f64>,
144
145 #[serde(rename = "updateTime")]
147 pub update_time: Option<String>,
148
149 #[serde(rename = "delayTime")]
151 pub delay_time: Option<i64>,
152
153 pub bid: Option<f64>,
155
156 pub offer: Option<f64>,
158
159 pub high: Option<f64>,
161
162 pub low: Option<f64>,
164
165 #[serde(rename = "binaryOdds")]
167 pub binary_odds: Option<f64>,
168
169 #[serde(rename = "decimalPlacesFactor")]
171 pub decimal_places_factor: Option<i64>,
172
173 #[serde(rename = "scalingFactor")]
175 pub scaling_factor: Option<i64>,
176
177 #[serde(rename = "controlledRiskExtraSpread")]
179 pub controlled_risk_extra_spread: Option<f64>,
180}
181
182#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
184pub struct MarketData {
185 pub epic: String,
187 #[serde(rename = "instrumentName")]
189 pub instrument_name: String,
190 #[serde(rename = "instrumentType")]
192 pub instrument_type: InstrumentType,
193 pub expiry: String,
195 #[serde(rename = "highLimitPrice")]
197 pub high_limit_price: Option<f64>,
198 #[serde(rename = "lowLimitPrice")]
200 pub low_limit_price: Option<f64>,
201 #[serde(rename = "marketStatus")]
203 pub market_status: String,
204 #[serde(rename = "netChange")]
206 pub net_change: Option<f64>,
207 #[serde(rename = "percentageChange")]
209 pub percentage_change: Option<f64>,
210 #[serde(rename = "updateTime")]
212 pub update_time: Option<String>,
213 #[serde(rename = "updateTimeUTC")]
215 pub update_time_utc: Option<String>,
216 pub bid: Option<f64>,
218 pub offer: Option<f64>,
220}
221
222impl MarketData {
223 pub fn is_call(&self) -> bool {
236 self.instrument_name.contains("CALL")
237 }
238
239 pub fn is_put(&self) -> bool {
251 self.instrument_name.contains("PUT")
252 }
253}
254
255#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
257pub struct HistoricalPrice {
258 #[serde(rename = "snapshotTime")]
260 pub snapshot_time: String,
261 #[serde(rename = "openPrice")]
263 pub open_price: PricePoint,
264 #[serde(rename = "highPrice")]
266 pub high_price: PricePoint,
267 #[serde(rename = "lowPrice")]
269 pub low_price: PricePoint,
270 #[serde(rename = "closePrice")]
272 pub close_price: PricePoint,
273 #[serde(rename = "lastTradedVolume")]
275 pub last_traded_volume: Option<i64>,
276}
277
278#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
280pub struct PricePoint {
281 pub bid: Option<f64>,
283 pub ask: Option<f64>,
285 #[serde(rename = "lastTraded")]
287 pub last_traded: Option<f64>,
288}
289
290#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
292pub struct PriceAllowance {
293 #[serde(rename = "remainingAllowance")]
295 pub remaining_allowance: i64,
296 #[serde(rename = "totalAllowance")]
298 pub total_allowance: i64,
299 #[serde(rename = "allowanceExpiry")]
301 pub allowance_expiry: i64,
302}
303
304#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
306pub struct ExpiryDetails {
307 #[serde(rename = "lastDealingDate")]
309 pub last_dealing_date: String,
310
311 #[serde(rename = "settlementInfo")]
313 pub settlement_info: Option<String>,
314}
315
316#[repr(u8)]
317#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
318pub enum StepUnit {
320 #[serde(rename = "POINTS")]
321 Points,
323 #[serde(rename = "PERCENTAGE")]
324 Percentage,
326 #[serde(rename = "pct")]
327 Pct,
329}
330
331#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
333pub struct StepDistance {
334 pub unit: Option<StepUnit>,
336 pub value: Option<f64>,
338}
339
340#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
342pub struct MarketNavigationNode {
343 pub id: String,
345 pub name: String,
347}
348
349#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
351pub struct MarketNode {
352 pub id: String,
354 pub name: String,
356 #[serde(skip_serializing_if = "Vec::is_empty", default)]
358 pub children: Vec<MarketNode>,
359 #[serde(skip_serializing_if = "Vec::is_empty", default)]
361 pub markets: Vec<MarketData>,
362}
363
364#[repr(u8)]
366#[derive(
367 DebugPretty, DisplaySimple, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Default,
368)]
369#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
370pub enum MarketState {
371 Closed,
373 #[default]
375 Offline,
376 Tradeable,
378 Edit,
380 EditsOnly,
382 Auction,
384 AuctionNoEdit,
386 Suspended,
388 OnAuction,
390 OnAuctionNoEdits,
392}
393
394#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
396pub struct PresentationMarketData {
397 pub item_name: String,
399 pub item_pos: i32,
401 pub fields: MarketFields,
403 pub changed_fields: MarketFields,
405 pub is_snapshot: bool,
407}
408
409impl PresentationMarketData {
410 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
418 let item_name = item_update.item_name.clone().unwrap_or_default();
420
421 let item_pos = item_update.item_pos as i32;
423
424 let is_snapshot = item_update.is_snapshot;
426
427 let fields = Self::create_market_fields(&item_update.fields)?;
429
430 let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
432 for (key, value) in &item_update.changed_fields {
433 changed_fields_map.insert(key.clone(), Some(value.clone()));
434 }
435 let changed_fields = Self::create_market_fields(&changed_fields_map)?;
436
437 Ok(PresentationMarketData {
438 item_name,
439 item_pos,
440 fields,
441 changed_fields,
442 is_snapshot,
443 })
444 }
445
446 fn create_market_fields(
454 fields_map: &HashMap<String, Option<String>>,
455 ) -> Result<MarketFields, String> {
456 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
458
459 let market_state = match get_field("MARKET_STATE").as_deref() {
461 Some("closed") => Some(MarketState::Closed),
462 Some("offline") => Some(MarketState::Offline),
463 Some("tradeable") => Some(MarketState::Tradeable),
464 Some("edit") => Some(MarketState::Edit),
465 Some("auction") => Some(MarketState::Auction),
466 Some("auction_no_edit") => Some(MarketState::AuctionNoEdit),
467 Some("suspended") => Some(MarketState::Suspended),
468 Some("on_auction") => Some(MarketState::OnAuction),
469 Some("on_auction_no_edit") => Some(MarketState::OnAuctionNoEdits),
470 Some(unknown) => return Err(format!("Unknown market state: {unknown}")),
471 None => None,
472 };
473
474 let market_delay = match get_field("MARKET_DELAY").as_deref() {
476 Some("0") => Some(false),
477 Some("1") => Some(true),
478 Some(val) => return Err(format!("Invalid MARKET_DELAY value: {val}")),
479 None => None,
480 };
481
482 let parse_float = |key: &str| -> Result<Option<f64>, String> {
484 match get_field(key) {
485 Some(val) if !val.is_empty() => val
486 .parse::<f64>()
487 .map(Some)
488 .map_err(|_| format!("Failed to parse {key} as float: {val}")),
489 _ => Ok(None),
490 }
491 };
492
493 Ok(MarketFields {
494 mid_open: parse_float("MID_OPEN")?,
495 high: parse_float("HIGH")?,
496 offer: parse_float("OFFER")?,
497 change: parse_float("CHANGE")?,
498 market_delay,
499 low: parse_float("LOW")?,
500 bid: parse_float("BID")?,
501 change_pct: parse_float("CHANGE_PCT")?,
502 market_state,
503 update_time: get_field("UPDATE_TIME"),
504 })
505 }
506}
507
508impl From<&ItemUpdate> for PresentationMarketData {
509 fn from(item_update: &ItemUpdate) -> Self {
510 Self::from_item_update(item_update).unwrap_or_else(|_| PresentationMarketData {
511 item_name: String::new(),
512 item_pos: 0,
513 fields: MarketFields::default(),
514 changed_fields: MarketFields::default(),
515 is_snapshot: false,
516 })
517 }
518}
519
520#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
522pub struct Category {
523 pub code: String,
525 #[serde(rename = "nonTradeable")]
527 pub non_tradeable: bool,
528}
529
530#[repr(u8)]
532#[derive(
533 DebugPretty, DisplaySimple, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash,
534)]
535#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
536pub enum CategoryMarketStatus {
537 #[default]
539 Offline,
540 Closed,
542 Suspended,
544 OnAuction,
546 OnAuctionNoEdits,
548 EditsOnly,
550 ClosingsOnly,
552 DealNoEdit,
554 Tradeable,
556}
557
558#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
560pub struct CategoryInstrument {
561 pub epic: String,
563 #[serde(rename = "instrumentName")]
565 pub instrument_name: String,
566 pub expiry: String,
568 #[serde(rename = "instrumentType")]
570 pub instrument_type: InstrumentType,
571 #[serde(rename = "lotSize")]
573 pub lot_size: Option<f64>,
574 #[serde(rename = "otcTradeable")]
576 pub otc_tradeable: bool,
577 #[serde(rename = "marketStatus")]
579 pub market_status: CategoryMarketStatus,
580 #[serde(rename = "delayTime")]
582 pub delay_time: Option<i64>,
583 pub bid: Option<f64>,
585 pub offer: Option<f64>,
587 pub high: Option<f64>,
589 pub low: Option<f64>,
591 #[serde(rename = "netChange")]
593 pub net_change: Option<f64>,
594 #[serde(rename = "percentageChange")]
596 pub percentage_change: Option<f64>,
597 #[serde(rename = "updateTime")]
599 pub update_time: Option<String>,
600 #[serde(rename = "scalingFactor")]
602 pub scaling_factor: Option<i64>,
603}
604
605#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
607pub struct CategoryInstrumentsMetadata {
608 #[serde(rename = "pageNumber")]
610 pub page_number: i32,
611 #[serde(rename = "pageSize")]
613 pub page_size: i32,
614}
615
616#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
618pub struct MarketFields {
619 #[serde(rename = "MID_OPEN")]
621 #[serde(with = "string_as_float_opt")]
622 #[serde(default)]
623 pub mid_open: Option<f64>,
624
625 #[serde(rename = "HIGH")]
627 #[serde(with = "string_as_float_opt")]
628 #[serde(default)]
629 pub high: Option<f64>,
630
631 #[serde(rename = "OFFER")]
633 #[serde(with = "string_as_float_opt")]
634 #[serde(default)]
635 pub offer: Option<f64>,
636
637 #[serde(rename = "CHANGE")]
639 #[serde(with = "string_as_float_opt")]
640 #[serde(default)]
641 pub change: Option<f64>,
642
643 #[serde(rename = "MARKET_DELAY")]
645 #[serde(with = "string_as_bool_opt")]
646 #[serde(default)]
647 pub market_delay: Option<bool>,
648
649 #[serde(rename = "LOW")]
651 #[serde(with = "string_as_float_opt")]
652 #[serde(default)]
653 pub low: Option<f64>,
654
655 #[serde(rename = "BID")]
657 #[serde(with = "string_as_float_opt")]
658 #[serde(default)]
659 pub bid: Option<f64>,
660
661 #[serde(rename = "CHANGE_PCT")]
663 #[serde(with = "string_as_float_opt")]
664 #[serde(default)]
665 pub change_pct: Option<f64>,
666
667 #[serde(rename = "MARKET_STATE")]
669 #[serde(default)]
670 pub market_state: Option<MarketState>,
671
672 #[serde(rename = "UPDATE_TIME")]
674 #[serde(default)]
675 pub update_time: Option<String>,
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 #[test]
683 fn test_market_data_is_call_returns_true_for_call_option() {
684 let market = MarketData {
685 epic: "test".to_string(),
686 instrument_name: "DAX CALL 18000".to_string(),
687 instrument_type: InstrumentType::default(),
688 expiry: "-".to_string(),
689 high_limit_price: None,
690 low_limit_price: None,
691 market_status: "TRADEABLE".to_string(),
692 net_change: None,
693 percentage_change: None,
694 update_time: None,
695 update_time_utc: None,
696 bid: Some(100.0),
697 offer: Some(101.0),
698 };
699 assert!(market.is_call());
700 assert!(!market.is_put());
701 }
702
703 #[test]
704 fn test_market_data_is_put_returns_true_for_put_option() {
705 let market = MarketData {
706 epic: "test".to_string(),
707 instrument_name: "DAX PUT 17000".to_string(),
708 instrument_type: InstrumentType::default(),
709 expiry: "-".to_string(),
710 high_limit_price: None,
711 low_limit_price: None,
712 market_status: "TRADEABLE".to_string(),
713 net_change: None,
714 percentage_change: None,
715 update_time: None,
716 update_time_utc: None,
717 bid: Some(50.0),
718 offer: Some(51.0),
719 };
720 assert!(market.is_put());
721 assert!(!market.is_call());
722 }
723
724 #[test]
725 fn test_market_data_neither_call_nor_put() {
726 let market = MarketData {
727 epic: "IX.D.DAX.DAILY.IP".to_string(),
728 instrument_name: "Germany 40".to_string(),
729 instrument_type: InstrumentType::default(),
730 expiry: "-".to_string(),
731 high_limit_price: None,
732 low_limit_price: None,
733 market_status: "TRADEABLE".to_string(),
734 net_change: None,
735 percentage_change: None,
736 update_time: None,
737 update_time_utc: None,
738 bid: Some(18000.0),
739 offer: Some(18001.0),
740 };
741 assert!(!market.is_call());
742 assert!(!market.is_put());
743 }
744
745 #[test]
746 fn test_market_state_default() {
747 let state = MarketState::default();
748 assert_eq!(state, MarketState::Offline);
749 }
750
751 #[test]
752 fn test_category_market_status_default() {
753 let status = CategoryMarketStatus::default();
754 assert_eq!(status, CategoryMarketStatus::Offline);
755 }
756
757 #[test]
758 fn test_step_unit_serialization() {
759 let points = StepUnit::Points;
760 let json = serde_json::to_string(&points).expect("serialize failed");
761 assert_eq!(json, "\"POINTS\"");
762
763 let pct = StepUnit::Percentage;
764 let json = serde_json::to_string(&pct).expect("serialize failed");
765 assert_eq!(json, "\"PERCENTAGE\"");
766 }
767
768 #[test]
769 fn test_step_distance_creation() {
770 let distance = StepDistance {
771 unit: Some(StepUnit::Points),
772 value: Some(1.5),
773 };
774 assert_eq!(distance.unit, Some(StepUnit::Points));
775 assert_eq!(distance.value, Some(1.5));
776 }
777
778 #[test]
779 fn test_market_fields_default() {
780 let fields = MarketFields::default();
781 assert!(fields.mid_open.is_none());
782 assert!(fields.high.is_none());
783 assert!(fields.offer.is_none());
784 assert!(fields.change.is_none());
785 assert!(fields.market_delay.is_none());
786 assert!(fields.low.is_none());
787 assert!(fields.bid.is_none());
788 assert!(fields.change_pct.is_none());
789 assert!(fields.market_state.is_none());
790 assert!(fields.update_time.is_none());
791 }
792
793 #[test]
794 fn test_presentation_market_data_default() {
795 let data = PresentationMarketData::default();
796 assert!(data.item_name.is_empty());
797 assert_eq!(data.item_pos, 0);
798 assert!(!data.is_snapshot);
799 }
800
801 #[test]
802 fn test_category_default() {
803 let cat = Category::default();
804 assert!(cat.code.is_empty());
805 assert!(!cat.non_tradeable);
806 }
807
808 #[test]
809 fn test_category_instrument_default() {
810 let inst = CategoryInstrument::default();
811 assert!(inst.epic.is_empty());
812 assert!(inst.instrument_name.is_empty());
813 assert_eq!(inst.market_status, CategoryMarketStatus::Offline);
814 }
815
816 #[test]
817 fn test_market_state_serialization() {
818 let tradeable = MarketState::Tradeable;
819 let json = serde_json::to_string(&tradeable).expect("serialize failed");
820 assert_eq!(json, "\"TRADEABLE\"");
821
822 let closed = MarketState::Closed;
823 let json = serde_json::to_string(&closed).expect("serialize failed");
824 assert_eq!(json, "\"CLOSED\"");
825 }
826
827 #[test]
828 fn test_price_point_creation() {
829 let point = PricePoint {
830 bid: Some(100.5),
831 ask: Some(101.0),
832 last_traded: Some(100.75),
833 };
834 assert_eq!(point.bid, Some(100.5));
835 assert_eq!(point.ask, Some(101.0));
836 assert_eq!(point.last_traded, Some(100.75));
837 }
838
839 #[test]
840 fn test_price_allowance_creation() {
841 let allowance = PriceAllowance {
842 remaining_allowance: 100,
843 total_allowance: 1000,
844 allowance_expiry: 3600,
845 };
846 assert_eq!(allowance.remaining_allowance, 100);
847 assert_eq!(allowance.total_allowance, 1000);
848 assert_eq!(allowance.allowance_expiry, 3600);
849 }
850
851 #[test]
852 fn test_expiry_details_creation() {
853 let expiry = ExpiryDetails {
854 last_dealing_date: "2024-12-31".to_string(),
855 settlement_info: Some("Cash settlement".to_string()),
856 };
857 assert_eq!(expiry.last_dealing_date, "2024-12-31");
858 assert_eq!(expiry.settlement_info, Some("Cash settlement".to_string()));
859 }
860
861 #[test]
862 fn test_market_navigation_node_creation() {
863 let node = MarketNavigationNode {
864 id: "12345".to_string(),
865 name: "Indices".to_string(),
866 };
867 assert_eq!(node.id, "12345");
868 assert_eq!(node.name, "Indices");
869 }
870
871 #[test]
872 fn test_market_node_creation() {
873 let node = MarketNode {
874 id: "node1".to_string(),
875 name: "Test Node".to_string(),
876 children: Vec::new(),
877 markets: Vec::new(),
878 };
879 assert_eq!(node.id, "node1");
880 assert_eq!(node.name, "Test Node");
881 assert!(node.children.is_empty());
882 assert!(node.markets.is_empty());
883 }
884
885 #[test]
886 fn test_currency_creation() {
887 let currency = Currency {
888 code: "USD".to_string(),
889 symbol: Some("$".to_string()),
890 base_exchange_rate: Some(1.0),
891 exchange_rate: Some(1.0),
892 is_default: Some(true),
893 };
894 assert_eq!(currency.code, "USD");
895 assert_eq!(currency.symbol, Some("$".to_string()));
896 assert_eq!(currency.is_default, Some(true));
897 }
898
899 #[test]
900 fn test_create_market_fields_with_valid_data() {
901 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
902 fields_map.insert("BID".to_string(), Some("100.5".to_string()));
903 fields_map.insert("OFFER".to_string(), Some("101.0".to_string()));
904 fields_map.insert("HIGH".to_string(), Some("102.0".to_string()));
905 fields_map.insert("LOW".to_string(), Some("99.0".to_string()));
906 fields_map.insert("CHANGE".to_string(), Some("1.5".to_string()));
907 fields_map.insert("CHANGE_PCT".to_string(), Some("1.5".to_string()));
908 fields_map.insert("MARKET_STATE".to_string(), Some("tradeable".to_string()));
909 fields_map.insert("MARKET_DELAY".to_string(), Some("0".to_string()));
910 fields_map.insert("UPDATE_TIME".to_string(), Some("12:30:00".to_string()));
911
912 let result = PresentationMarketData::create_market_fields(&fields_map);
913 assert!(result.is_ok());
914
915 let fields = result.expect("should parse");
916 assert_eq!(fields.bid, Some(100.5));
917 assert_eq!(fields.offer, Some(101.0));
918 assert_eq!(fields.high, Some(102.0));
919 assert_eq!(fields.low, Some(99.0));
920 assert_eq!(fields.change, Some(1.5));
921 assert_eq!(fields.market_state, Some(MarketState::Tradeable));
922 assert_eq!(fields.market_delay, Some(false));
923 assert_eq!(fields.update_time, Some("12:30:00".to_string()));
924 }
925
926 #[test]
927 fn test_create_market_fields_with_empty_map() {
928 let fields_map: HashMap<String, Option<String>> = HashMap::new();
929 let result = PresentationMarketData::create_market_fields(&fields_map);
930 assert!(result.is_ok());
931
932 let fields = result.expect("should parse");
933 assert!(fields.bid.is_none());
934 assert!(fields.offer.is_none());
935 }
936
937 #[test]
938 fn test_create_market_fields_invalid_market_state() {
939 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
940 fields_map.insert(
941 "MARKET_STATE".to_string(),
942 Some("invalid_state".to_string()),
943 );
944
945 let result = PresentationMarketData::create_market_fields(&fields_map);
946 assert!(result.is_err());
947 }
948
949 #[test]
950 fn test_create_market_fields_invalid_market_delay() {
951 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
952 fields_map.insert("MARKET_DELAY".to_string(), Some("invalid".to_string()));
953
954 let result = PresentationMarketData::create_market_fields(&fields_map);
955 assert!(result.is_err());
956 }
957
958 #[test]
959 fn test_create_market_fields_all_market_states() {
960 let states = vec![
961 ("closed", MarketState::Closed),
962 ("offline", MarketState::Offline),
963 ("tradeable", MarketState::Tradeable),
964 ("edit", MarketState::Edit),
965 ("auction", MarketState::Auction),
966 ("auction_no_edit", MarketState::AuctionNoEdit),
967 ("suspended", MarketState::Suspended),
968 ("on_auction", MarketState::OnAuction),
969 ("on_auction_no_edit", MarketState::OnAuctionNoEdits),
970 ];
971
972 for (state_str, expected_state) in states {
973 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
974 fields_map.insert("MARKET_STATE".to_string(), Some(state_str.to_string()));
975
976 let result = PresentationMarketData::create_market_fields(&fields_map);
977 assert!(result.is_ok(), "Failed for state: {}", state_str);
978 let fields = result.expect("should parse");
979 assert_eq!(fields.market_state, Some(expected_state));
980 }
981 }
982
983 #[test]
984 fn test_market_delay_values() {
985 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
986 fields_map.insert("MARKET_DELAY".to_string(), Some("1".to_string()));
987
988 let result = PresentationMarketData::create_market_fields(&fields_map);
989 assert!(result.is_ok());
990 let fields = result.expect("should parse");
991 assert_eq!(fields.market_delay, Some(true));
992 }
993}