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: StepDistance,
96
97    /// Minimum deal size allowed
98    #[serde(rename = "minDealSize")]
99    pub min_deal_size: StepDistance,
100
101    /// Minimum distance for controlled risk stop
102    #[serde(rename = "minControlledRiskStopDistance")]
103    pub min_controlled_risk_stop_distance: StepDistance,
104
105    /// Minimum distance for normal stop or limit orders
106    #[serde(rename = "minNormalStopOrLimitDistance")]
107    pub min_normal_stop_or_limit_distance: StepDistance,
108
109    /// Maximum distance for stop or limit orders
110    #[serde(rename = "maxStopOrLimitDistance")]
111    pub max_stop_or_limit_distance: StepDistance,
112
113    /// Controlled risk spacing
114    #[serde(rename = "controlledRiskSpacing")]
115    pub controlled_risk_spacing: 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317/// Unit for step distances in trading rules
318pub enum StepUnit {
319    #[serde(rename = "POINTS")]
320    /// Points (price movement units)
321    Points,
322    #[serde(rename = "PERCENTAGE")]
323    /// Percentage value
324    Percentage,
325    #[serde(rename = "pct")]
326    /// Alternative representation for percentage
327    Pct,
328}
329
330/// A struct to handle the minStepDistance value which can be a complex object
331#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
332pub struct StepDistance {
333    /// Unit type for the distance
334    pub unit: Option<StepUnit>,
335    /// Numeric value of the distance
336    pub value: Option<f64>,
337}
338
339/// Node in the market navigation hierarchy
340#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
341pub struct MarketNavigationNode {
342    /// Unique identifier for the node
343    pub id: String,
344    /// Display name of the node
345    pub name: String,
346}
347
348/// Structure representing a node in the market hierarchy
349#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
350pub struct MarketNode {
351    /// Node ID
352    pub id: String,
353    /// Node name
354    pub name: String,
355    /// Child nodes
356    #[serde(skip_serializing_if = "Vec::is_empty", default)]
357    pub children: Vec<MarketNode>,
358    /// Markets in this node
359    #[serde(skip_serializing_if = "Vec::is_empty", default)]
360    pub markets: Vec<MarketData>,
361}
362
363/// Represents the current state of a market
364#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize, Clone, PartialEq, Default)]
365#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
366pub enum MarketState {
367    /// Market is closed for trading
368    Closed,
369    /// Market is offline and not available
370    #[default]
371    Offline,
372    /// Market is open and available for trading
373    Tradeable,
374    /// Market is in edit mode
375    Edit,
376    /// Market is in edit mode only (no new positions, only edits allowed)
377    EditsOnly,
378    /// Market is in auction phase
379    Auction,
380    /// Market is in auction phase but editing is not allowed
381    AuctionNoEdit,
382    /// Market is temporarily suspended
383    Suspended,
384    /// Market is in auction phase
385    OnAuction,
386    /// Market is in auction phase but editing is not allowed
387    OnAuctionNoEdits,
388}
389
390/// Representation of market data received from the IG Markets streaming API
391#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
392pub struct PresentationMarketData {
393    /// Name of the item this data belongs to
394    pub item_name: String,
395    /// Position of the item in the subscription
396    pub item_pos: i32,
397    /// All market fields
398    pub fields: MarketFields,
399    /// Fields that have changed in this update
400    pub changed_fields: MarketFields,
401    /// Whether this is a snapshot or an update
402    pub is_snapshot: bool,
403}
404
405impl PresentationMarketData {
406    /// Converts an ItemUpdate from the Lightstreamer API to a MarketData object
407    ///
408    /// # Arguments
409    /// * `item_update` - The ItemUpdate received from the Lightstreamer API
410    ///
411    /// # Returns
412    /// * `Result<Self, String>` - The converted MarketData or an error message
413    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
414        // Extract the item_name, defaulting to an empty string if None
415        let item_name = item_update.item_name.clone().unwrap_or_default();
416
417        // Convert item_pos from usize to i32
418        let item_pos = item_update.item_pos as i32;
419
420        // Extract is_snapshot
421        let is_snapshot = item_update.is_snapshot;
422
423        // Convert fields
424        let fields = Self::create_market_fields(&item_update.fields)?;
425
426        // Convert changed_fields by first creating a HashMap<String, Option<String>>
427        let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
428        for (key, value) in &item_update.changed_fields {
429            changed_fields_map.insert(key.clone(), Some(value.clone()));
430        }
431        let changed_fields = Self::create_market_fields(&changed_fields_map)?;
432
433        Ok(PresentationMarketData {
434            item_name,
435            item_pos,
436            fields,
437            changed_fields,
438            is_snapshot,
439        })
440    }
441
442    /// Helper method to create MarketFields from a HashMap of field values
443    ///
444    /// # Arguments
445    /// * `fields_map` - HashMap containing field names and their string values
446    ///
447    /// # Returns
448    /// * `Result<MarketFields, String>` - The parsed MarketFields or an error message
449    fn create_market_fields(
450        fields_map: &HashMap<String, Option<String>>,
451    ) -> Result<MarketFields, String> {
452        // Helper function to safely get a field value
453        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
454
455        // Parse market state
456        let market_state = match get_field("MARKET_STATE").as_deref() {
457            Some("closed") => Some(MarketState::Closed),
458            Some("offline") => Some(MarketState::Offline),
459            Some("tradeable") => Some(MarketState::Tradeable),
460            Some("edit") => Some(MarketState::Edit),
461            Some("auction") => Some(MarketState::Auction),
462            Some("auction_no_edit") => Some(MarketState::AuctionNoEdit),
463            Some("suspended") => Some(MarketState::Suspended),
464            Some("on_auction") => Some(MarketState::OnAuction),
465            Some("on_auction_no_edit") => Some(MarketState::OnAuctionNoEdits),
466            Some(unknown) => return Err(format!("Unknown market state: {unknown}")),
467            None => None,
468        };
469
470        // Parse boolean field
471        let market_delay = match get_field("MARKET_DELAY").as_deref() {
472            Some("0") => Some(false),
473            Some("1") => Some(true),
474            Some(val) => return Err(format!("Invalid MARKET_DELAY value: {val}")),
475            None => None,
476        };
477
478        // Helper function to parse float values
479        let parse_float = |key: &str| -> Result<Option<f64>, String> {
480            match get_field(key) {
481                Some(val) if !val.is_empty() => val
482                    .parse::<f64>()
483                    .map(Some)
484                    .map_err(|_| format!("Failed to parse {key} as float: {val}")),
485                _ => Ok(None),
486            }
487        };
488
489        Ok(MarketFields {
490            mid_open: parse_float("MID_OPEN")?,
491            high: parse_float("HIGH")?,
492            offer: parse_float("OFFER")?,
493            change: parse_float("CHANGE")?,
494            market_delay,
495            low: parse_float("LOW")?,
496            bid: parse_float("BID")?,
497            change_pct: parse_float("CHANGE_PCT")?,
498            market_state,
499            update_time: get_field("UPDATE_TIME"),
500        })
501    }
502}
503
504impl From<&ItemUpdate> for PresentationMarketData {
505    fn from(item_update: &ItemUpdate) -> Self {
506        Self::from_item_update(item_update).unwrap_or_else(|_| PresentationMarketData {
507            item_name: String::new(),
508            item_pos: 0,
509            fields: MarketFields::default(),
510            changed_fields: MarketFields::default(),
511            is_snapshot: false,
512        })
513    }
514}
515
516/// Represents a category of instruments in the IG Markets API
517#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
518pub struct Category {
519    /// Category code identifier
520    pub code: String,
521    /// True if the category is non-tradeable
522    #[serde(rename = "nonTradeable")]
523    pub non_tradeable: bool,
524}
525
526/// Market status for category instruments
527#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
528#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
529pub enum CategoryMarketStatus {
530    /// Market is offline
531    #[default]
532    Offline,
533    /// Market is closed
534    Closed,
535    /// Market is suspended
536    Suspended,
537    /// Market is in auction mode
538    OnAuction,
539    /// Market is in no-edits mode
540    OnAuctionNoEdits,
541    /// Market is open for edits only
542    EditsOnly,
543    /// Market allows closings only
544    ClosingsOnly,
545    /// Market allows deals but not edits
546    DealNoEdit,
547    /// Market is open for trades
548    Tradeable,
549}
550
551/// Represents an instrument within a category
552#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
553pub struct CategoryInstrument {
554    /// Unique instrument identifier (EPIC)
555    pub epic: String,
556    /// Name of the instrument
557    #[serde(rename = "instrumentName")]
558    pub instrument_name: String,
559    /// Expiry date of the instrument
560    pub expiry: String,
561    /// Type of the instrument
562    #[serde(rename = "instrumentType")]
563    pub instrument_type: InstrumentType,
564    /// Size of an instrument lot
565    #[serde(rename = "lotSize")]
566    pub lot_size: Option<f64>,
567    /// True if the instrument can be traded OTC
568    #[serde(rename = "otcTradeable")]
569    pub otc_tradeable: bool,
570    /// Current status of the market
571    #[serde(rename = "marketStatus")]
572    pub market_status: CategoryMarketStatus,
573    /// Price delay time for market data in minutes
574    #[serde(rename = "delayTime")]
575    pub delay_time: Option<i64>,
576    /// Current bid price
577    pub bid: Option<f64>,
578    /// Current offer price
579    pub offer: Option<f64>,
580    /// Highest price for the current session
581    pub high: Option<f64>,
582    /// Lowest price for the current session
583    pub low: Option<f64>,
584    /// Net change in price
585    #[serde(rename = "netChange")]
586    pub net_change: Option<f64>,
587    /// Percentage change in price
588    #[serde(rename = "percentageChange")]
589    pub percentage_change: Option<f64>,
590    /// Time of last price update
591    #[serde(rename = "updateTime")]
592    pub update_time: Option<String>,
593    /// Multiplying factor to determine actual pip value
594    #[serde(rename = "scalingFactor")]
595    pub scaling_factor: Option<i64>,
596}
597
598/// Paging metadata for category instruments response
599#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
600pub struct CategoryInstrumentsMetadata {
601    /// Current page number
602    #[serde(rename = "pageNumber")]
603    pub page_number: i32,
604    /// Number of items per page
605    #[serde(rename = "pageSize")]
606    pub page_size: i32,
607}
608
609/// Fields containing market price and status information
610#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
611pub struct MarketFields {
612    /// The mid-open price of the market
613    #[serde(rename = "MID_OPEN")]
614    #[serde(with = "string_as_float_opt")]
615    #[serde(default)]
616    pub mid_open: Option<f64>,
617
618    /// The highest price reached by the market in the current trading session
619    #[serde(rename = "HIGH")]
620    #[serde(with = "string_as_float_opt")]
621    #[serde(default)]
622    pub high: Option<f64>,
623
624    /// The current offer (ask) price of the market
625    #[serde(rename = "OFFER")]
626    #[serde(with = "string_as_float_opt")]
627    #[serde(default)]
628    pub offer: Option<f64>,
629
630    /// The absolute price change since the previous close
631    #[serde(rename = "CHANGE")]
632    #[serde(with = "string_as_float_opt")]
633    #[serde(default)]
634    pub change: Option<f64>,
635
636    /// Indicates if there is a delay in market data
637    #[serde(rename = "MARKET_DELAY")]
638    #[serde(with = "string_as_bool_opt")]
639    #[serde(default)]
640    pub market_delay: Option<bool>,
641
642    /// The lowest price reached by the market in the current trading session
643    #[serde(rename = "LOW")]
644    #[serde(with = "string_as_float_opt")]
645    #[serde(default)]
646    pub low: Option<f64>,
647
648    /// The current bid price of the market
649    #[serde(rename = "BID")]
650    #[serde(with = "string_as_float_opt")]
651    #[serde(default)]
652    pub bid: Option<f64>,
653
654    /// The percentage price change since the previous close
655    #[serde(rename = "CHANGE_PCT")]
656    #[serde(with = "string_as_float_opt")]
657    #[serde(default)]
658    pub change_pct: Option<f64>,
659
660    /// The current state of the market (e.g., Tradeable, Closed, etc.)
661    #[serde(rename = "MARKET_STATE")]
662    #[serde(default)]
663    pub market_state: Option<MarketState>,
664
665    /// The timestamp of the last market update
666    #[serde(rename = "UPDATE_TIME")]
667    #[serde(default)]
668    pub update_time: Option<String>,
669}