Skip to main content

ig_client/presentation/
market.rs

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/// Model for a market instrument with enhanced deserialization
9#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
10pub struct Instrument {
11    /// Unique identifier for the instrument
12    pub epic: String,
13    /// Human-readable name of the instrument
14    pub name: String,
15    /// Expiry date of the instrument
16    pub expiry: String,
17    /// Size of one contract
18    #[serde(rename = "contractSize")]
19    pub contract_size: String,
20    /// Size of one lot
21    #[serde(rename = "lotSize")]
22    pub lot_size: Option<f64>,
23    /// Upper price limit for the instrument
24    #[serde(rename = "highLimitPrice")]
25    pub high_limit_price: Option<f64>,
26    /// Lower price limit for the instrument
27    #[serde(rename = "lowLimitPrice")]
28    pub low_limit_price: Option<f64>,
29    /// Margin factor for the instrument
30    #[serde(rename = "marginFactor")]
31    pub margin_factor: Option<f64>,
32    /// Unit for the margin factor
33    #[serde(rename = "marginFactorUnit")]
34    pub margin_factor_unit: Option<String>,
35    /// Available currencies for trading this instrument
36    pub currencies: Option<Vec<Currency>>,
37    #[serde(rename = "valueOfOnePip")]
38    /// Value of one pip for this instrument
39    pub value_of_one_pip: String,
40    /// Type of the instrument
41    #[serde(rename = "instrumentType")]
42    pub instrument_type: Option<InstrumentType>,
43    /// Expiry details including last dealing date
44    #[serde(rename = "expiryDetails")]
45    pub expiry_details: Option<ExpiryDetails>,
46    #[serde(rename = "slippageFactor")]
47    /// Slippage factor for the instrument
48    pub slippage_factor: Option<StepDistance>,
49    #[serde(rename = "limitedRiskPremium")]
50    /// Premium for limited risk trades
51    pub limited_risk_premium: Option<StepDistance>,
52    #[serde(rename = "newsCode")]
53    /// Code used for news related to this instrument
54    pub news_code: Option<String>,
55    #[serde(rename = "chartCode")]
56    /// Code used for charting this instrument
57    pub chart_code: Option<String>,
58}
59
60/// Model for an instrument's currency
61#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
62pub struct Currency {
63    /// Currency code (e.g., "USD", "EUR")
64    pub code: String,
65    /// Currency symbol (e.g., "$", "€")
66    pub symbol: Option<String>,
67    /// Base exchange rate for the currency
68    #[serde(rename = "baseExchangeRate")]
69    pub base_exchange_rate: Option<f64>,
70    /// Current exchange rate
71    #[serde(rename = "exchangeRate")]
72    pub exchange_rate: Option<f64>,
73    /// Whether this is the default currency for the instrument
74    #[serde(rename = "isDefault")]
75    pub is_default: Option<bool>,
76}
77
78/// Model for market data with enhanced deserialization
79#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
80pub struct MarketDetails {
81    /// Detailed information about the instrument
82    pub instrument: Instrument,
83    /// Current market snapshot with prices
84    pub snapshot: MarketSnapshot,
85    /// Trading rules for the market
86    #[serde(rename = "dealingRules")]
87    pub dealing_rules: DealingRules,
88}
89
90/// Trading rules for a market with enhanced deserialization
91#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
92pub struct DealingRules {
93    /// Minimum step distance
94    #[serde(rename = "minStepDistance")]
95    pub min_step_distance: Option<StepDistance>,
96
97    /// Minimum deal size allowed
98    #[serde(rename = "minDealSize")]
99    pub min_deal_size: Option<StepDistance>,
100
101    /// Minimum distance for controlled risk stop
102    #[serde(rename = "minControlledRiskStopDistance")]
103    pub min_controlled_risk_stop_distance: Option<StepDistance>,
104
105    /// Minimum distance for normal stop or limit orders
106    #[serde(rename = "minNormalStopOrLimitDistance")]
107    pub min_normal_stop_or_limit_distance: Option<StepDistance>,
108
109    /// Maximum distance for stop or limit orders
110    #[serde(rename = "maxStopOrLimitDistance")]
111    pub max_stop_or_limit_distance: Option<StepDistance>,
112
113    /// Controlled risk spacing
114    #[serde(rename = "controlledRiskSpacing")]
115    pub controlled_risk_spacing: Option<StepDistance>,
116
117    /// Market order preference setting
118    #[serde(rename = "marketOrderPreference")]
119    pub market_order_preference: String,
120
121    /// Trailing stops preference setting
122    #[serde(rename = "trailingStopsPreference")]
123    pub trailing_stops_preference: String,
124
125    #[serde(rename = "maxDealSize")]
126    /// Maximum deal size allowed
127    pub max_deal_size: Option<f64>,
128}
129
130/// Market snapshot with enhanced deserialization
131#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
132pub struct MarketSnapshot {
133    /// Current status of the market (e.g., "OPEN", "CLOSED")
134    #[serde(rename = "marketStatus")]
135    pub market_status: String,
136
137    /// Net change in price since previous close
138    #[serde(rename = "netChange")]
139    pub net_change: Option<f64>,
140
141    /// Percentage change in price since previous close
142    #[serde(rename = "percentageChange")]
143    pub percentage_change: Option<f64>,
144
145    /// Time of the last price update
146    #[serde(rename = "updateTime")]
147    pub update_time: Option<String>,
148
149    /// Delay time in milliseconds for market data
150    #[serde(rename = "delayTime")]
151    pub delay_time: Option<i64>,
152
153    /// Current bid price
154    pub bid: Option<f64>,
155
156    /// Current offer/ask price
157    pub offer: Option<f64>,
158
159    /// Highest price of the current trading session
160    pub high: Option<f64>,
161
162    /// Lowest price of the current trading session
163    pub low: Option<f64>,
164
165    /// Odds for binary markets
166    #[serde(rename = "binaryOdds")]
167    pub binary_odds: Option<f64>,
168
169    /// Factor for decimal places in price display
170    #[serde(rename = "decimalPlacesFactor")]
171    pub decimal_places_factor: Option<i64>,
172
173    /// Factor for scaling prices
174    #[serde(rename = "scalingFactor")]
175    pub scaling_factor: Option<i64>,
176
177    /// Extra spread for controlled risk trades
178    #[serde(rename = "controlledRiskExtraSpread")]
179    pub controlled_risk_extra_spread: Option<f64>,
180}
181
182/// Basic market data
183#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
184pub struct MarketData {
185    /// Unique identifier for the market
186    pub epic: String,
187    /// Human-readable name of the instrument
188    #[serde(rename = "instrumentName")]
189    pub instrument_name: String,
190    /// Type of the instrument
191    #[serde(rename = "instrumentType")]
192    pub instrument_type: InstrumentType,
193    /// Expiry date of the instrument
194    pub expiry: String,
195    /// Upper price limit for the market
196    #[serde(rename = "highLimitPrice")]
197    pub high_limit_price: Option<f64>,
198    /// Lower price limit for the market
199    #[serde(rename = "lowLimitPrice")]
200    pub low_limit_price: Option<f64>,
201    /// Current status of the market
202    #[serde(rename = "marketStatus")]
203    pub market_status: String,
204    /// Net change in price since previous close
205    #[serde(rename = "netChange")]
206    pub net_change: Option<f64>,
207    /// Percentage change in price since previous close
208    #[serde(rename = "percentageChange")]
209    pub percentage_change: Option<f64>,
210    /// Time of the last price update
211    #[serde(rename = "updateTime")]
212    pub update_time: Option<String>,
213    /// Time of the last price update in UTC
214    #[serde(rename = "updateTimeUTC")]
215    pub update_time_utc: Option<String>,
216    /// Current bid price
217    pub bid: Option<f64>,
218    /// Current offer/ask price
219    pub offer: Option<f64>,
220}
221
222impl MarketData {
223    /// Checks if the current financial instrument is a call option.
224    ///
225    /// A call option is a financial derivative that gives the holder the right (but not the obligation)
226    /// to buy an underlying asset at a specified price within a specified time period. This method checks
227    /// whether the instrument represented by this instance is a call option by inspecting the `instrument_name`
228    /// field.
229    ///
230    /// # Returns
231    ///
232    /// * `true` if the instrument's name contains the substring `"CALL"`, indicating it is a call option.
233    /// * `false` otherwise.
234    ///
235    pub fn is_call(&self) -> bool {
236        self.instrument_name.contains("CALL")
237    }
238
239    /// Checks if the financial instrument is a "PUT" option.
240    ///
241    /// This method examines the `instrument_name` field of the struct to determine
242    /// if it contains the substring "PUT". If the substring is found, the method
243    /// returns `true`, indicating that the instrument is categorized as a "PUT" option.
244    /// Otherwise, it returns `false`.
245    ///
246    /// # Returns
247    /// * `true` - If `instrument_name` contains the substring "PUT".
248    /// * `false` - If `instrument_name` does not contain the substring "PUT".
249    ///
250    pub fn is_put(&self) -> bool {
251        self.instrument_name.contains("PUT")
252    }
253}
254
255/// Historical price data point
256#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
257pub struct HistoricalPrice {
258    /// Timestamp of the price data point
259    #[serde(rename = "snapshotTime")]
260    pub snapshot_time: String,
261    /// Opening price for the period
262    #[serde(rename = "openPrice")]
263    pub open_price: PricePoint,
264    /// Highest price for the period
265    #[serde(rename = "highPrice")]
266    pub high_price: PricePoint,
267    /// Lowest price for the period
268    #[serde(rename = "lowPrice")]
269    pub low_price: PricePoint,
270    /// Closing price for the period
271    #[serde(rename = "closePrice")]
272    pub close_price: PricePoint,
273    /// Volume traded during the period
274    #[serde(rename = "lastTradedVolume")]
275    pub last_traded_volume: Option<i64>,
276}
277
278/// Price point with bid, ask and last traded prices
279#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
280pub struct PricePoint {
281    /// Bid price at this point
282    pub bid: Option<f64>,
283    /// Ask/offer price at this point
284    pub ask: Option<f64>,
285    /// Last traded price at this point
286    #[serde(rename = "lastTraded")]
287    pub last_traded: Option<f64>,
288}
289
290/// Information about API usage allowance for price data
291#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
292pub struct PriceAllowance {
293    /// Remaining API calls allowed in the current period
294    #[serde(rename = "remainingAllowance")]
295    pub remaining_allowance: i64,
296    /// Total API calls allowed per period
297    #[serde(rename = "totalAllowance")]
298    pub total_allowance: i64,
299    /// Time until the allowance resets
300    #[serde(rename = "allowanceExpiry")]
301    pub allowance_expiry: i64,
302}
303
304/// Details about instrument expiry
305#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
306pub struct ExpiryDetails {
307    /// The last dealing date and time for the instrument
308    #[serde(rename = "lastDealingDate")]
309    pub last_dealing_date: String,
310
311    /// Information about settlement
312    #[serde(rename = "settlementInfo")]
313    pub settlement_info: Option<String>,
314}
315
316#[repr(u8)]
317#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
318/// Unit for step distances in trading rules
319pub enum StepUnit {
320    #[serde(rename = "POINTS")]
321    /// Points (price movement units)
322    Points,
323    #[serde(rename = "PERCENTAGE")]
324    /// Percentage value
325    Percentage,
326    #[serde(rename = "pct")]
327    /// Alternative representation for percentage
328    Pct,
329}
330
331/// A struct to handle the minStepDistance value which can be a complex object
332#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
333pub struct StepDistance {
334    /// Unit type for the distance
335    pub unit: Option<StepUnit>,
336    /// Numeric value of the distance
337    pub value: Option<f64>,
338}
339
340/// Node in the market navigation hierarchy
341#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
342pub struct MarketNavigationNode {
343    /// Unique identifier for the node
344    pub id: String,
345    /// Display name of the node
346    pub name: String,
347}
348
349/// Structure representing a node in the market hierarchy
350#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
351pub struct MarketNode {
352    /// Node ID
353    pub id: String,
354    /// Node name
355    pub name: String,
356    /// Child nodes
357    #[serde(skip_serializing_if = "Vec::is_empty", default)]
358    pub children: Vec<MarketNode>,
359    /// Markets in this node
360    #[serde(skip_serializing_if = "Vec::is_empty", default)]
361    pub markets: Vec<MarketData>,
362}
363
364/// Represents the current state of a market
365#[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    /// Market is closed for trading
372    Closed,
373    /// Market is offline and not available
374    #[default]
375    Offline,
376    /// Market is open and available for trading
377    Tradeable,
378    /// Market is in edit mode
379    Edit,
380    /// Market is in edit mode only (no new positions, only edits allowed)
381    EditsOnly,
382    /// Market is in auction phase
383    Auction,
384    /// Market is in auction phase but editing is not allowed
385    AuctionNoEdit,
386    /// Market is temporarily suspended
387    Suspended,
388    /// Market is in auction phase
389    OnAuction,
390    /// Market is in auction phase but editing is not allowed
391    OnAuctionNoEdits,
392}
393
394/// Representation of market data received from the IG Markets streaming API
395#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
396pub struct PresentationMarketData {
397    /// Name of the item this data belongs to
398    pub item_name: String,
399    /// Position of the item in the subscription
400    pub item_pos: i32,
401    /// All market fields
402    pub fields: MarketFields,
403    /// Fields that have changed in this update
404    pub changed_fields: MarketFields,
405    /// Whether this is a snapshot or an update
406    pub is_snapshot: bool,
407}
408
409impl PresentationMarketData {
410    /// Converts an ItemUpdate from the Lightstreamer API to a MarketData object
411    ///
412    /// # Arguments
413    /// * `item_update` - The ItemUpdate received from the Lightstreamer API
414    ///
415    /// # Returns
416    /// * `Result<Self, String>` - The converted MarketData or an error message
417    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
418        // Extract the item_name, defaulting to an empty string if None
419        let item_name = item_update.item_name.clone().unwrap_or_default();
420
421        // Convert item_pos from usize to i32
422        let item_pos = item_update.item_pos as i32;
423
424        // Extract is_snapshot
425        let is_snapshot = item_update.is_snapshot;
426
427        // Convert fields
428        let fields = Self::create_market_fields(&item_update.fields)?;
429
430        // Convert changed_fields by first creating a HashMap<String, Option<String>>
431        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    /// Helper method to create MarketFields from a HashMap of field values
447    ///
448    /// # Arguments
449    /// * `fields_map` - HashMap containing field names and their string values
450    ///
451    /// # Returns
452    /// * `Result<MarketFields, String>` - The parsed MarketFields or an error message
453    fn create_market_fields(
454        fields_map: &HashMap<String, Option<String>>,
455    ) -> Result<MarketFields, String> {
456        // Helper function to safely get a field value
457        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
458
459        // Parse market state
460        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        // Parse boolean field
475        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        // Helper function to parse float values
483        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/// Represents a category of instruments in the IG Markets API
521#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
522pub struct Category {
523    /// Category code identifier
524    pub code: String,
525    /// True if the category is non-tradeable
526    #[serde(rename = "nonTradeable")]
527    pub non_tradeable: bool,
528}
529
530/// Market status for category instruments
531#[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    /// Market is offline
538    #[default]
539    Offline,
540    /// Market is closed
541    Closed,
542    /// Market is suspended
543    Suspended,
544    /// Market is in auction mode
545    OnAuction,
546    /// Market is in no-edits mode
547    OnAuctionNoEdits,
548    /// Market is open for edits only
549    EditsOnly,
550    /// Market allows closings only
551    ClosingsOnly,
552    /// Market allows deals but not edits
553    DealNoEdit,
554    /// Market is open for trades
555    Tradeable,
556}
557
558/// Represents an instrument within a category
559#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
560pub struct CategoryInstrument {
561    /// Unique instrument identifier (EPIC)
562    pub epic: String,
563    /// Name of the instrument
564    #[serde(rename = "instrumentName")]
565    pub instrument_name: String,
566    /// Expiry date of the instrument
567    pub expiry: String,
568    /// Type of the instrument
569    #[serde(rename = "instrumentType")]
570    pub instrument_type: InstrumentType,
571    /// Size of an instrument lot
572    #[serde(rename = "lotSize")]
573    pub lot_size: Option<f64>,
574    /// True if the instrument can be traded OTC
575    #[serde(rename = "otcTradeable")]
576    pub otc_tradeable: bool,
577    /// Current status of the market
578    #[serde(rename = "marketStatus")]
579    pub market_status: CategoryMarketStatus,
580    /// Price delay time for market data in minutes
581    #[serde(rename = "delayTime")]
582    pub delay_time: Option<i64>,
583    /// Current bid price
584    pub bid: Option<f64>,
585    /// Current offer price
586    pub offer: Option<f64>,
587    /// Highest price for the current session
588    pub high: Option<f64>,
589    /// Lowest price for the current session
590    pub low: Option<f64>,
591    /// Net change in price
592    #[serde(rename = "netChange")]
593    pub net_change: Option<f64>,
594    /// Percentage change in price
595    #[serde(rename = "percentageChange")]
596    pub percentage_change: Option<f64>,
597    /// Time of last price update
598    #[serde(rename = "updateTime")]
599    pub update_time: Option<String>,
600    /// Multiplying factor to determine actual pip value
601    #[serde(rename = "scalingFactor")]
602    pub scaling_factor: Option<i64>,
603}
604
605/// Paging metadata for category instruments response
606#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
607pub struct CategoryInstrumentsMetadata {
608    /// Current page number
609    #[serde(rename = "pageNumber")]
610    pub page_number: i32,
611    /// Number of items per page
612    #[serde(rename = "pageSize")]
613    pub page_size: i32,
614}
615
616/// Fields containing market price and status information
617#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
618pub struct MarketFields {
619    /// The mid-open price of the market
620    #[serde(rename = "MID_OPEN")]
621    #[serde(with = "string_as_float_opt")]
622    #[serde(default)]
623    pub mid_open: Option<f64>,
624
625    /// The highest price reached by the market in the current trading session
626    #[serde(rename = "HIGH")]
627    #[serde(with = "string_as_float_opt")]
628    #[serde(default)]
629    pub high: Option<f64>,
630
631    /// The current offer (ask) price of the market
632    #[serde(rename = "OFFER")]
633    #[serde(with = "string_as_float_opt")]
634    #[serde(default)]
635    pub offer: Option<f64>,
636
637    /// The absolute price change since the previous close
638    #[serde(rename = "CHANGE")]
639    #[serde(with = "string_as_float_opt")]
640    #[serde(default)]
641    pub change: Option<f64>,
642
643    /// Indicates if there is a delay in market data
644    #[serde(rename = "MARKET_DELAY")]
645    #[serde(with = "string_as_bool_opt")]
646    #[serde(default)]
647    pub market_delay: Option<bool>,
648
649    /// The lowest price reached by the market in the current trading session
650    #[serde(rename = "LOW")]
651    #[serde(with = "string_as_float_opt")]
652    #[serde(default)]
653    pub low: Option<f64>,
654
655    /// The current bid price of the market
656    #[serde(rename = "BID")]
657    #[serde(with = "string_as_float_opt")]
658    #[serde(default)]
659    pub bid: Option<f64>,
660
661    /// The percentage price change since the previous close
662    #[serde(rename = "CHANGE_PCT")]
663    #[serde(with = "string_as_float_opt")]
664    #[serde(default)]
665    pub change_pct: Option<f64>,
666
667    /// The current state of the market (e.g., Tradeable, Closed, etc.)
668    #[serde(rename = "MARKET_STATE")]
669    #[serde(default)]
670    pub market_state: Option<MarketState>,
671
672    /// The timestamp of the last market update
673    #[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}