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 serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::fmt;
7use std::fmt::Display;
8
9/// Model for a market instrument with enhanced deserialization
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct Instrument {
12    /// Unique identifier for the instrument
13    pub epic: String,
14    /// Human-readable name of the instrument
15    pub name: String,
16    /// Expiry date of the instrument
17    pub expiry: String,
18    /// Size of one contract
19    #[serde(rename = "contractSize")]
20    pub contract_size: String,
21    /// Size of one lot
22    #[serde(rename = "lotSize")]
23    pub lot_size: Option<f64>,
24    /// Upper price limit for the instrument
25    #[serde(rename = "highLimitPrice")]
26    pub high_limit_price: Option<f64>,
27    /// Lower price limit for the instrument
28    #[serde(rename = "lowLimitPrice")]
29    pub low_limit_price: Option<f64>,
30    /// Margin factor for the instrument
31    #[serde(rename = "marginFactor")]
32    pub margin_factor: Option<f64>,
33    /// Unit for the margin factor
34    #[serde(rename = "marginFactorUnit")]
35    pub margin_factor_unit: Option<String>,
36    /// Available currencies for trading this instrument
37    pub currencies: Option<Vec<Currency>>,
38    #[serde(rename = "valueOfOnePip")]
39    /// Value of one pip for this instrument
40    pub value_of_one_pip: String,
41    /// Type of the instrument
42    #[serde(rename = "instrumentType")]
43    pub instrument_type: Option<InstrumentType>,
44    /// Expiry details including last dealing date
45    #[serde(rename = "expiryDetails")]
46    pub expiry_details: Option<ExpiryDetails>,
47    #[serde(rename = "slippageFactor")]
48    /// Slippage factor for the instrument
49    pub slippage_factor: Option<StepDistance>,
50    #[serde(rename = "limitedRiskPremium")]
51    /// Premium for limited risk trades
52    pub limited_risk_premium: Option<StepDistance>,
53    #[serde(rename = "newsCode")]
54    /// Code used for news related to this instrument
55    pub news_code: Option<String>,
56    #[serde(rename = "chartCode")]
57    /// Code used for charting this instrument
58    pub chart_code: Option<String>,
59}
60
61/// Model for an instrument's currency
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct Currency {
64    /// Currency code (e.g., "USD", "EUR")
65    pub code: String,
66    /// Currency symbol (e.g., "$", "€")
67    pub symbol: Option<String>,
68    /// Base exchange rate for the currency
69    #[serde(rename = "baseExchangeRate")]
70    pub base_exchange_rate: Option<f64>,
71    /// Current exchange rate
72    #[serde(rename = "exchangeRate")]
73    pub exchange_rate: Option<f64>,
74    /// Whether this is the default currency for the instrument
75    #[serde(rename = "isDefault")]
76    pub is_default: Option<bool>,
77}
78
79/// Model for market data with enhanced deserialization
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct MarketDetails {
82    /// Detailed information about the instrument
83    pub instrument: Instrument,
84    /// Current market snapshot with prices
85    pub snapshot: MarketSnapshot,
86    /// Trading rules for the market
87    #[serde(rename = "dealingRules")]
88    pub dealing_rules: DealingRules,
89}
90
91/// Trading rules for a market with enhanced deserialization
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DealingRules {
94    /// Minimum step distance
95    #[serde(rename = "minStepDistance")]
96    pub min_step_distance: StepDistance,
97
98    /// Minimum deal size allowed
99    #[serde(rename = "minDealSize")]
100    pub min_deal_size: StepDistance,
101
102    /// Minimum distance for controlled risk stop
103    #[serde(rename = "minControlledRiskStopDistance")]
104    pub min_controlled_risk_stop_distance: StepDistance,
105
106    /// Minimum distance for normal stop or limit orders
107    #[serde(rename = "minNormalStopOrLimitDistance")]
108    pub min_normal_stop_or_limit_distance: StepDistance,
109
110    /// Maximum distance for stop or limit orders
111    #[serde(rename = "maxStopOrLimitDistance")]
112    pub max_stop_or_limit_distance: StepDistance,
113
114    /// Controlled risk spacing
115    #[serde(rename = "controlledRiskSpacing")]
116    pub controlled_risk_spacing: StepDistance,
117
118    /// Market order preference setting
119    #[serde(rename = "marketOrderPreference")]
120    pub market_order_preference: String,
121
122    /// Trailing stops preference setting
123    #[serde(rename = "trailingStopsPreference")]
124    pub trailing_stops_preference: String,
125
126    #[serde(rename = "maxDealSize")]
127    /// Maximum deal size allowed
128    pub max_deal_size: Option<f64>,
129}
130
131/// Market snapshot with enhanced deserialization
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct MarketSnapshot {
134    /// Current status of the market (e.g., "OPEN", "CLOSED")
135    #[serde(rename = "marketStatus")]
136    pub market_status: String,
137
138    /// Net change in price since previous close
139    #[serde(rename = "netChange")]
140    pub net_change: Option<f64>,
141
142    /// Percentage change in price since previous close
143    #[serde(rename = "percentageChange")]
144    pub percentage_change: Option<f64>,
145
146    /// Time of the last price update
147    #[serde(rename = "updateTime")]
148    pub update_time: Option<String>,
149
150    /// Delay time in milliseconds for market data
151    #[serde(rename = "delayTime")]
152    pub delay_time: Option<i64>,
153
154    /// Current bid price
155    pub bid: Option<f64>,
156
157    /// Current offer/ask price
158    pub offer: Option<f64>,
159
160    /// Highest price of the current trading session
161    pub high: Option<f64>,
162
163    /// Lowest price of the current trading session
164    pub low: Option<f64>,
165
166    /// Odds for binary markets
167    #[serde(rename = "binaryOdds")]
168    pub binary_odds: Option<f64>,
169
170    /// Factor for decimal places in price display
171    #[serde(rename = "decimalPlacesFactor")]
172    pub decimal_places_factor: Option<i64>,
173
174    /// Factor for scaling prices
175    #[serde(rename = "scalingFactor")]
176    pub scaling_factor: Option<i64>,
177
178    /// Extra spread for controlled risk trades
179    #[serde(rename = "controlledRiskExtraSpread")]
180    pub controlled_risk_extra_spread: Option<f64>,
181}
182
183/// Basic market data
184#[derive(Debug, Clone, Deserialize, Serialize)]
185pub struct MarketData {
186    /// Unique identifier for the market
187    pub epic: String,
188    /// Human-readable name of the instrument
189    #[serde(rename = "instrumentName")]
190    pub instrument_name: String,
191    /// Type of the instrument
192    #[serde(rename = "instrumentType")]
193    pub instrument_type: InstrumentType,
194    /// Expiry date of the instrument
195    pub expiry: String,
196    /// Upper price limit for the market
197    #[serde(rename = "highLimitPrice")]
198    pub high_limit_price: Option<f64>,
199    /// Lower price limit for the market
200    #[serde(rename = "lowLimitPrice")]
201    pub low_limit_price: Option<f64>,
202    /// Current status of the market
203    #[serde(rename = "marketStatus")]
204    pub market_status: String,
205    /// Net change in price since previous close
206    #[serde(rename = "netChange")]
207    pub net_change: Option<f64>,
208    /// Percentage change in price since previous close
209    #[serde(rename = "percentageChange")]
210    pub percentage_change: Option<f64>,
211    /// Time of the last price update
212    #[serde(rename = "updateTime")]
213    pub update_time: Option<String>,
214    /// Time of the last price update in UTC
215    #[serde(rename = "updateTimeUTC")]
216    pub update_time_utc: Option<String>,
217    /// Current bid price
218    pub bid: Option<f64>,
219    /// Current offer/ask price
220    pub offer: Option<f64>,
221}
222
223impl Display for MarketData {
224    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225        let json = serde_json::to_string(self).unwrap_or_else(|_| "Invalid JSON".to_string());
226        write!(f, "{json}")
227    }
228}
229
230/// Historical price data point
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct HistoricalPrice {
233    /// Timestamp of the price data point
234    #[serde(rename = "snapshotTime")]
235    pub snapshot_time: String,
236    /// Opening price for the period
237    #[serde(rename = "openPrice")]
238    pub open_price: PricePoint,
239    /// Highest price for the period
240    #[serde(rename = "highPrice")]
241    pub high_price: PricePoint,
242    /// Lowest price for the period
243    #[serde(rename = "lowPrice")]
244    pub low_price: PricePoint,
245    /// Closing price for the period
246    #[serde(rename = "closePrice")]
247    pub close_price: PricePoint,
248    /// Volume traded during the period
249    #[serde(rename = "lastTradedVolume")]
250    pub last_traded_volume: Option<i64>,
251}
252
253/// Price point with bid, ask and last traded prices
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct PricePoint {
256    /// Bid price at this point
257    pub bid: Option<f64>,
258    /// Ask/offer price at this point
259    pub ask: Option<f64>,
260    /// Last traded price at this point
261    #[serde(rename = "lastTraded")]
262    pub last_traded: Option<f64>,
263}
264
265/// Information about API usage allowance for price data
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct PriceAllowance {
268    /// Remaining API calls allowed in the current period
269    #[serde(rename = "remainingAllowance")]
270    pub remaining_allowance: i64,
271    /// Total API calls allowed per period
272    #[serde(rename = "totalAllowance")]
273    pub total_allowance: i64,
274    /// Time until the allowance resets
275    #[serde(rename = "allowanceExpiry")]
276    pub allowance_expiry: i64,
277}
278
279/// Details about instrument expiry
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
281pub struct ExpiryDetails {
282    /// The last dealing date and time for the instrument
283    #[serde(rename = "lastDealingDate")]
284    pub last_dealing_date: String,
285
286    /// Information about settlement
287    #[serde(rename = "settlementInfo")]
288    pub settlement_info: Option<String>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
292/// Unit for step distances in trading rules
293pub enum StepUnit {
294    #[serde(rename = "POINTS")]
295    /// Points (price movement units)
296    Points,
297    #[serde(rename = "PERCENTAGE")]
298    /// Percentage value
299    Percentage,
300    #[serde(rename = "pct")]
301    /// Alternative representation for percentage
302    Pct,
303}
304
305/// A struct to handle the minStepDistance value which can be a complex object
306#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
307pub struct StepDistance {
308    /// Unit type for the distance
309    pub unit: Option<StepUnit>,
310    /// Numeric value of the distance
311    pub value: Option<f64>,
312}
313
314/// Node in the market navigation hierarchy
315#[derive(Debug, Clone, Deserialize, Serialize)]
316pub struct MarketNavigationNode {
317    /// Unique identifier for the node
318    pub id: String,
319    /// Display name of the node
320    pub name: String,
321}
322
323/// Structure representing a node in the market hierarchy
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct MarketNode {
326    /// Node ID
327    pub id: String,
328    /// Node name
329    pub name: String,
330    /// Child nodes
331    #[serde(skip_serializing_if = "Vec::is_empty", default)]
332    pub children: Vec<MarketNode>,
333    /// Markets in this node
334    #[serde(skip_serializing_if = "Vec::is_empty", default)]
335    pub markets: Vec<MarketData>,
336}
337
338/// Represents the current state of a market
339#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
340#[serde(rename_all = "UPPERCASE")]
341pub enum MarketState {
342    /// Market is closed for trading
343    Closed,
344    /// Market is offline and not available
345    #[default]
346    Offline,
347    /// Market is open and available for trading
348    Tradeable,
349    /// Market is in edit mode
350    Edit,
351    /// Market is in auction phase
352    Auction,
353    /// Market is in auction phase but editing is not allowed
354    AuctionNoEdit,
355    /// Market is temporarily suspended
356    Suspended,
357}
358
359/// Representation of market data received from the IG Markets streaming API
360#[derive(Debug, Clone, Serialize, Deserialize, Default)]
361pub struct PresentationMarketData {
362    /// Name of the item this data belongs to
363    pub item_name: String,
364    /// Position of the item in the subscription
365    pub item_pos: i32,
366    /// All market fields
367    pub fields: MarketFields,
368    /// Fields that have changed in this update
369    pub changed_fields: MarketFields,
370    /// Whether this is a snapshot or an update
371    pub is_snapshot: bool,
372}
373
374impl PresentationMarketData {
375    /// Converts an ItemUpdate from the Lightstreamer API to a MarketData object
376    ///
377    /// # Arguments
378    /// * `item_update` - The ItemUpdate received from the Lightstreamer API
379    ///
380    /// # Returns
381    /// * `Result<Self, String>` - The converted MarketData or an error message
382    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
383        // Extract the item_name, defaulting to an empty string if None
384        let item_name = item_update.item_name.clone().unwrap_or_default();
385
386        // Convert item_pos from usize to i32
387        let item_pos = item_update.item_pos as i32;
388
389        // Extract is_snapshot
390        let is_snapshot = item_update.is_snapshot;
391
392        // Convert fields
393        let fields = Self::create_market_fields(&item_update.fields)?;
394
395        // Convert changed_fields by first creating a HashMap<String, Option<String>>
396        let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
397        for (key, value) in &item_update.changed_fields {
398            changed_fields_map.insert(key.clone(), Some(value.clone()));
399        }
400        let changed_fields = Self::create_market_fields(&changed_fields_map)?;
401
402        Ok(PresentationMarketData {
403            item_name,
404            item_pos,
405            fields,
406            changed_fields,
407            is_snapshot,
408        })
409    }
410
411    /// Helper method to create MarketFields from a HashMap of field values
412    ///
413    /// # Arguments
414    /// * `fields_map` - HashMap containing field names and their string values
415    ///
416    /// # Returns
417    /// * `Result<MarketFields, String>` - The parsed MarketFields or an error message
418    fn create_market_fields(
419        fields_map: &HashMap<String, Option<String>>,
420    ) -> Result<MarketFields, String> {
421        // Helper function to safely get a field value
422        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
423
424        // Parse market state
425        let market_state = match get_field("MARKET_STATE").as_deref() {
426            Some("closed") => Some(MarketState::Closed),
427            Some("offline") => Some(MarketState::Offline),
428            Some("tradeable") => Some(MarketState::Tradeable),
429            Some("edit") => Some(MarketState::Edit),
430            Some("auction") => Some(MarketState::Auction),
431            Some("auction_no_edit") => Some(MarketState::AuctionNoEdit),
432            Some("suspended") => Some(MarketState::Suspended),
433            Some(unknown) => return Err(format!("Unknown market state: {unknown}")),
434            None => None,
435        };
436
437        // Parse boolean field
438        let market_delay = match get_field("MARKET_DELAY").as_deref() {
439            Some("0") => Some(false),
440            Some("1") => Some(true),
441            Some(val) => return Err(format!("Invalid MARKET_DELAY value: {val}")),
442            None => None,
443        };
444
445        // Helper function to parse float values
446        let parse_float = |key: &str| -> Result<Option<f64>, String> {
447            match get_field(key) {
448                Some(val) if !val.is_empty() => val
449                    .parse::<f64>()
450                    .map(Some)
451                    .map_err(|_| format!("Failed to parse {key} as float: {val}")),
452                _ => Ok(None),
453            }
454        };
455
456        Ok(MarketFields {
457            mid_open: parse_float("MID_OPEN")?,
458            high: parse_float("HIGH")?,
459            offer: parse_float("OFFER")?,
460            change: parse_float("CHANGE")?,
461            market_delay,
462            low: parse_float("LOW")?,
463            bid: parse_float("BID")?,
464            change_pct: parse_float("CHANGE_PCT")?,
465            market_state,
466            update_time: get_field("UPDATE_TIME"),
467        })
468    }
469}
470
471impl fmt::Display for PresentationMarketData {
472    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473        let json = serde_json::to_string(self).map_err(|_| fmt::Error)?;
474        write!(f, "{json}")
475    }
476}
477
478impl From<&ItemUpdate> for PresentationMarketData {
479    fn from(item_update: &ItemUpdate) -> Self {
480        Self::from_item_update(item_update).unwrap_or_else(|_| PresentationMarketData {
481            item_name: String::new(),
482            item_pos: 0,
483            fields: MarketFields::default(),
484            changed_fields: MarketFields::default(),
485            is_snapshot: false,
486        })
487    }
488}
489
490/// Fields containing market price and status information
491#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
492pub struct MarketFields {
493    /// The mid-open price of the market
494    #[serde(rename = "MID_OPEN")]
495    #[serde(with = "string_as_float_opt")]
496    #[serde(default)]
497    pub mid_open: Option<f64>,
498
499    /// The highest price reached by the market in the current trading session
500    #[serde(rename = "HIGH")]
501    #[serde(with = "string_as_float_opt")]
502    #[serde(default)]
503    pub high: Option<f64>,
504
505    /// The current offer (ask) price of the market
506    #[serde(rename = "OFFER")]
507    #[serde(with = "string_as_float_opt")]
508    #[serde(default)]
509    pub offer: Option<f64>,
510
511    /// The absolute price change since the previous close
512    #[serde(rename = "CHANGE")]
513    #[serde(with = "string_as_float_opt")]
514    #[serde(default)]
515    pub change: Option<f64>,
516
517    /// Indicates if there is a delay in market data
518    #[serde(rename = "MARKET_DELAY")]
519    #[serde(with = "string_as_bool_opt")]
520    #[serde(default)]
521    pub market_delay: Option<bool>,
522
523    /// The lowest price reached by the market in the current trading session
524    #[serde(rename = "LOW")]
525    #[serde(with = "string_as_float_opt")]
526    #[serde(default)]
527    pub low: Option<f64>,
528
529    /// The current bid price of the market
530    #[serde(rename = "BID")]
531    #[serde(with = "string_as_float_opt")]
532    #[serde(default)]
533    pub bid: Option<f64>,
534
535    /// The percentage price change since the previous close
536    #[serde(rename = "CHANGE_PCT")]
537    #[serde(with = "string_as_float_opt")]
538    #[serde(default)]
539    pub change_pct: Option<f64>,
540
541    /// The current state of the market (e.g., Tradeable, Closed, etc.)
542    #[serde(rename = "MARKET_STATE")]
543    #[serde(default)]
544    pub market_state: Option<MarketState>,
545
546    /// The timestamp of the last market update
547    #[serde(rename = "UPDATE_TIME")]
548    #[serde(default)]
549    pub update_time: Option<String>,
550}