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 #[serde(rename = "expiryTimestamp")]
610 pub expiry_timestamp: Option<i64>,
611 #[serde(rename = "underlyingName")]
613 pub underlying_name: Option<String>,
614 pub popularity: Option<i64>,
616 #[serde(rename = "marketType")]
618 pub market_type: Option<String>,
619 #[serde(rename = "marketSubtype")]
621 pub market_subtype: Option<String>,
622}
623
624#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
626pub struct CategoryInstrumentsMetadata {
627 #[serde(rename = "pageNumber")]
629 pub page_number: i32,
630 #[serde(rename = "pageSize")]
632 pub page_size: i32,
633}
634
635#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
637pub struct MarketFields {
638 #[serde(rename = "MID_OPEN")]
640 #[serde(with = "string_as_float_opt")]
641 #[serde(default)]
642 pub mid_open: Option<f64>,
643
644 #[serde(rename = "HIGH")]
646 #[serde(with = "string_as_float_opt")]
647 #[serde(default)]
648 pub high: Option<f64>,
649
650 #[serde(rename = "OFFER")]
652 #[serde(with = "string_as_float_opt")]
653 #[serde(default)]
654 pub offer: Option<f64>,
655
656 #[serde(rename = "CHANGE")]
658 #[serde(with = "string_as_float_opt")]
659 #[serde(default)]
660 pub change: Option<f64>,
661
662 #[serde(rename = "MARKET_DELAY")]
664 #[serde(with = "string_as_bool_opt")]
665 #[serde(default)]
666 pub market_delay: Option<bool>,
667
668 #[serde(rename = "LOW")]
670 #[serde(with = "string_as_float_opt")]
671 #[serde(default)]
672 pub low: Option<f64>,
673
674 #[serde(rename = "BID")]
676 #[serde(with = "string_as_float_opt")]
677 #[serde(default)]
678 pub bid: Option<f64>,
679
680 #[serde(rename = "CHANGE_PCT")]
682 #[serde(with = "string_as_float_opt")]
683 #[serde(default)]
684 pub change_pct: Option<f64>,
685
686 #[serde(rename = "MARKET_STATE")]
688 #[serde(default)]
689 pub market_state: Option<MarketState>,
690
691 #[serde(rename = "UPDATE_TIME")]
693 #[serde(default)]
694 pub update_time: Option<String>,
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn test_market_data_is_call_returns_true_for_call_option() {
703 let market = MarketData {
704 epic: "test".to_string(),
705 instrument_name: "DAX CALL 18000".to_string(),
706 instrument_type: InstrumentType::default(),
707 expiry: "-".to_string(),
708 high_limit_price: None,
709 low_limit_price: None,
710 market_status: "TRADEABLE".to_string(),
711 net_change: None,
712 percentage_change: None,
713 update_time: None,
714 update_time_utc: None,
715 bid: Some(100.0),
716 offer: Some(101.0),
717 };
718 assert!(market.is_call());
719 assert!(!market.is_put());
720 }
721
722 #[test]
723 fn test_market_data_is_put_returns_true_for_put_option() {
724 let market = MarketData {
725 epic: "test".to_string(),
726 instrument_name: "DAX PUT 17000".to_string(),
727 instrument_type: InstrumentType::default(),
728 expiry: "-".to_string(),
729 high_limit_price: None,
730 low_limit_price: None,
731 market_status: "TRADEABLE".to_string(),
732 net_change: None,
733 percentage_change: None,
734 update_time: None,
735 update_time_utc: None,
736 bid: Some(50.0),
737 offer: Some(51.0),
738 };
739 assert!(market.is_put());
740 assert!(!market.is_call());
741 }
742
743 #[test]
744 fn test_market_data_neither_call_nor_put() {
745 let market = MarketData {
746 epic: "IX.D.DAX.DAILY.IP".to_string(),
747 instrument_name: "Germany 40".to_string(),
748 instrument_type: InstrumentType::default(),
749 expiry: "-".to_string(),
750 high_limit_price: None,
751 low_limit_price: None,
752 market_status: "TRADEABLE".to_string(),
753 net_change: None,
754 percentage_change: None,
755 update_time: None,
756 update_time_utc: None,
757 bid: Some(18000.0),
758 offer: Some(18001.0),
759 };
760 assert!(!market.is_call());
761 assert!(!market.is_put());
762 }
763
764 #[test]
765 fn test_market_state_default() {
766 let state = MarketState::default();
767 assert_eq!(state, MarketState::Offline);
768 }
769
770 #[test]
771 fn test_category_market_status_default() {
772 let status = CategoryMarketStatus::default();
773 assert_eq!(status, CategoryMarketStatus::Offline);
774 }
775
776 #[test]
777 fn test_step_unit_serialization() {
778 let points = StepUnit::Points;
779 let json = serde_json::to_string(&points).expect("serialize failed");
780 assert_eq!(json, "\"POINTS\"");
781
782 let pct = StepUnit::Percentage;
783 let json = serde_json::to_string(&pct).expect("serialize failed");
784 assert_eq!(json, "\"PERCENTAGE\"");
785 }
786
787 #[test]
788 fn test_step_distance_creation() {
789 let distance = StepDistance {
790 unit: Some(StepUnit::Points),
791 value: Some(1.5),
792 };
793 assert_eq!(distance.unit, Some(StepUnit::Points));
794 assert_eq!(distance.value, Some(1.5));
795 }
796
797 #[test]
798 fn test_market_fields_default() {
799 let fields = MarketFields::default();
800 assert!(fields.mid_open.is_none());
801 assert!(fields.high.is_none());
802 assert!(fields.offer.is_none());
803 assert!(fields.change.is_none());
804 assert!(fields.market_delay.is_none());
805 assert!(fields.low.is_none());
806 assert!(fields.bid.is_none());
807 assert!(fields.change_pct.is_none());
808 assert!(fields.market_state.is_none());
809 assert!(fields.update_time.is_none());
810 }
811
812 #[test]
813 fn test_presentation_market_data_default() {
814 let data = PresentationMarketData::default();
815 assert!(data.item_name.is_empty());
816 assert_eq!(data.item_pos, 0);
817 assert!(!data.is_snapshot);
818 }
819
820 #[test]
821 fn test_category_default() {
822 let cat = Category::default();
823 assert!(cat.code.is_empty());
824 assert!(!cat.non_tradeable);
825 }
826
827 #[test]
828 fn test_category_instrument_default() {
829 let inst = CategoryInstrument::default();
830 assert!(inst.epic.is_empty());
831 assert!(inst.instrument_name.is_empty());
832 assert_eq!(inst.market_status, CategoryMarketStatus::Offline);
833 assert!(inst.expiry_timestamp.is_none());
834 assert!(inst.underlying_name.is_none());
835 assert!(inst.popularity.is_none());
836 assert!(inst.market_type.is_none());
837 assert!(inst.market_subtype.is_none());
838 }
839
840 #[test]
841 fn test_category_instrument_deserialization_full_payload() {
842 let json = r#"{
843 "epic": "OD.D.OTCWK2AUD.28.IP",
844 "instrumentName": "Weekly AUDUSD ($1) 7200 Call",
845 "expiry": "17-JUL-26",
846 "instrumentType": "OPT_CURRENCIES",
847 "lotSize": 1.0,
848 "otcTradeable": true,
849 "marketStatus": "EDITS_ONLY",
850 "delayTime": 0,
851 "bid": 0.0,
852 "offer": 4.0,
853 "high": 4.0,
854 "low": 0.0,
855 "netChange": 6936.2,
856 "percentageChange": 0.0,
857 "updateTime": "06:53:51",
858 "scalingFactor": 1,
859 "underlyingName": "AUD/USD",
860 "popularity": 91783089620,
861 "marketType": "FX_PAIR",
862 "marketSubtype": "Fiat",
863 "expiryTimestamp": 1784300400000
864 }"#;
865
866 let inst: CategoryInstrument = serde_json::from_str(json).expect("deserialize failed");
867 assert_eq!(inst.epic, "OD.D.OTCWK2AUD.28.IP");
868 assert_eq!(inst.instrument_name, "Weekly AUDUSD ($1) 7200 Call");
869 assert_eq!(inst.expiry, "17-JUL-26");
870 assert_eq!(inst.market_status, CategoryMarketStatus::EditsOnly);
871 assert_eq!(inst.expiry_timestamp, Some(1784300400000));
872 assert_eq!(inst.underlying_name.as_deref(), Some("AUD/USD"));
873 assert_eq!(inst.popularity, Some(91783089620));
874 assert_eq!(inst.market_type.as_deref(), Some("FX_PAIR"));
875 assert_eq!(inst.market_subtype.as_deref(), Some("Fiat"));
876 }
877
878 #[test]
879 fn test_category_instrument_deserialization_without_optional_fields() {
880 let json = r#"{
881 "epic": "IX.D.FTSE.DAILY.IP",
882 "instrumentName": "FTSE 100",
883 "expiry": "-",
884 "instrumentType": "INDICES",
885 "otcTradeable": false,
886 "marketStatus": "TRADEABLE"
887 }"#;
888
889 let inst: CategoryInstrument = serde_json::from_str(json).expect("deserialize failed");
890 assert_eq!(inst.epic, "IX.D.FTSE.DAILY.IP");
891 assert!(inst.expiry_timestamp.is_none());
892 assert!(inst.underlying_name.is_none());
893 assert!(inst.popularity.is_none());
894 assert!(inst.market_type.is_none());
895 assert!(inst.market_subtype.is_none());
896 }
897
898 #[test]
899 fn test_market_state_serialization() {
900 let tradeable = MarketState::Tradeable;
901 let json = serde_json::to_string(&tradeable).expect("serialize failed");
902 assert_eq!(json, "\"TRADEABLE\"");
903
904 let closed = MarketState::Closed;
905 let json = serde_json::to_string(&closed).expect("serialize failed");
906 assert_eq!(json, "\"CLOSED\"");
907 }
908
909 #[test]
910 fn test_price_point_creation() {
911 let point = PricePoint {
912 bid: Some(100.5),
913 ask: Some(101.0),
914 last_traded: Some(100.75),
915 };
916 assert_eq!(point.bid, Some(100.5));
917 assert_eq!(point.ask, Some(101.0));
918 assert_eq!(point.last_traded, Some(100.75));
919 }
920
921 #[test]
922 fn test_price_allowance_creation() {
923 let allowance = PriceAllowance {
924 remaining_allowance: 100,
925 total_allowance: 1000,
926 allowance_expiry: 3600,
927 };
928 assert_eq!(allowance.remaining_allowance, 100);
929 assert_eq!(allowance.total_allowance, 1000);
930 assert_eq!(allowance.allowance_expiry, 3600);
931 }
932
933 #[test]
934 fn test_expiry_details_creation() {
935 let expiry = ExpiryDetails {
936 last_dealing_date: "2024-12-31".to_string(),
937 settlement_info: Some("Cash settlement".to_string()),
938 };
939 assert_eq!(expiry.last_dealing_date, "2024-12-31");
940 assert_eq!(expiry.settlement_info, Some("Cash settlement".to_string()));
941 }
942
943 #[test]
944 fn test_market_navigation_node_creation() {
945 let node = MarketNavigationNode {
946 id: "12345".to_string(),
947 name: "Indices".to_string(),
948 };
949 assert_eq!(node.id, "12345");
950 assert_eq!(node.name, "Indices");
951 }
952
953 #[test]
954 fn test_market_node_creation() {
955 let node = MarketNode {
956 id: "node1".to_string(),
957 name: "Test Node".to_string(),
958 children: Vec::new(),
959 markets: Vec::new(),
960 };
961 assert_eq!(node.id, "node1");
962 assert_eq!(node.name, "Test Node");
963 assert!(node.children.is_empty());
964 assert!(node.markets.is_empty());
965 }
966
967 #[test]
968 fn test_currency_creation() {
969 let currency = Currency {
970 code: "USD".to_string(),
971 symbol: Some("$".to_string()),
972 base_exchange_rate: Some(1.0),
973 exchange_rate: Some(1.0),
974 is_default: Some(true),
975 };
976 assert_eq!(currency.code, "USD");
977 assert_eq!(currency.symbol, Some("$".to_string()));
978 assert_eq!(currency.is_default, Some(true));
979 }
980
981 #[test]
982 fn test_create_market_fields_with_valid_data() {
983 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
984 fields_map.insert("BID".to_string(), Some("100.5".to_string()));
985 fields_map.insert("OFFER".to_string(), Some("101.0".to_string()));
986 fields_map.insert("HIGH".to_string(), Some("102.0".to_string()));
987 fields_map.insert("LOW".to_string(), Some("99.0".to_string()));
988 fields_map.insert("CHANGE".to_string(), Some("1.5".to_string()));
989 fields_map.insert("CHANGE_PCT".to_string(), Some("1.5".to_string()));
990 fields_map.insert("MARKET_STATE".to_string(), Some("tradeable".to_string()));
991 fields_map.insert("MARKET_DELAY".to_string(), Some("0".to_string()));
992 fields_map.insert("UPDATE_TIME".to_string(), Some("12:30:00".to_string()));
993
994 let result = PresentationMarketData::create_market_fields(&fields_map);
995 assert!(result.is_ok());
996
997 let fields = result.expect("should parse");
998 assert_eq!(fields.bid, Some(100.5));
999 assert_eq!(fields.offer, Some(101.0));
1000 assert_eq!(fields.high, Some(102.0));
1001 assert_eq!(fields.low, Some(99.0));
1002 assert_eq!(fields.change, Some(1.5));
1003 assert_eq!(fields.market_state, Some(MarketState::Tradeable));
1004 assert_eq!(fields.market_delay, Some(false));
1005 assert_eq!(fields.update_time, Some("12:30:00".to_string()));
1006 }
1007
1008 #[test]
1009 fn test_create_market_fields_with_empty_map() {
1010 let fields_map: HashMap<String, Option<String>> = HashMap::new();
1011 let result = PresentationMarketData::create_market_fields(&fields_map);
1012 assert!(result.is_ok());
1013
1014 let fields = result.expect("should parse");
1015 assert!(fields.bid.is_none());
1016 assert!(fields.offer.is_none());
1017 }
1018
1019 #[test]
1020 fn test_create_market_fields_invalid_market_state() {
1021 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1022 fields_map.insert(
1023 "MARKET_STATE".to_string(),
1024 Some("invalid_state".to_string()),
1025 );
1026
1027 let result = PresentationMarketData::create_market_fields(&fields_map);
1028 assert!(result.is_err());
1029 }
1030
1031 #[test]
1032 fn test_create_market_fields_invalid_market_delay() {
1033 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1034 fields_map.insert("MARKET_DELAY".to_string(), Some("invalid".to_string()));
1035
1036 let result = PresentationMarketData::create_market_fields(&fields_map);
1037 assert!(result.is_err());
1038 }
1039
1040 #[test]
1041 fn test_create_market_fields_all_market_states() {
1042 let states = vec![
1043 ("closed", MarketState::Closed),
1044 ("offline", MarketState::Offline),
1045 ("tradeable", MarketState::Tradeable),
1046 ("edit", MarketState::Edit),
1047 ("auction", MarketState::Auction),
1048 ("auction_no_edit", MarketState::AuctionNoEdit),
1049 ("suspended", MarketState::Suspended),
1050 ("on_auction", MarketState::OnAuction),
1051 ("on_auction_no_edit", MarketState::OnAuctionNoEdits),
1052 ];
1053
1054 for (state_str, expected_state) in states {
1055 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1056 fields_map.insert("MARKET_STATE".to_string(), Some(state_str.to_string()));
1057
1058 let result = PresentationMarketData::create_market_fields(&fields_map);
1059 assert!(result.is_ok(), "Failed for state: {}", state_str);
1060 let fields = result.expect("should parse");
1061 assert_eq!(fields.market_state, Some(expected_state));
1062 }
1063 }
1064
1065 #[test]
1066 fn test_market_delay_values() {
1067 let mut fields_map: HashMap<String, Option<String>> = HashMap::new();
1068 fields_map.insert("MARKET_DELAY".to_string(), Some("1".to_string()));
1069
1070 let result = PresentationMarketData::create_market_fields(&fields_map);
1071 assert!(result.is_ok());
1072 let fields = result.expect("should parse");
1073 assert_eq!(fields.market_delay, Some(true));
1074 }
1075}