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
222/// Historical price data point
223#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
224pub struct HistoricalPrice {
225    /// Timestamp of the price data point
226    #[serde(rename = "snapshotTime")]
227    pub snapshot_time: String,
228    /// Opening price for the period
229    #[serde(rename = "openPrice")]
230    pub open_price: PricePoint,
231    /// Highest price for the period
232    #[serde(rename = "highPrice")]
233    pub high_price: PricePoint,
234    /// Lowest price for the period
235    #[serde(rename = "lowPrice")]
236    pub low_price: PricePoint,
237    /// Closing price for the period
238    #[serde(rename = "closePrice")]
239    pub close_price: PricePoint,
240    /// Volume traded during the period
241    #[serde(rename = "lastTradedVolume")]
242    pub last_traded_volume: Option<i64>,
243}
244
245/// Price point with bid, ask and last traded prices
246#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
247pub struct PricePoint {
248    /// Bid price at this point
249    pub bid: Option<f64>,
250    /// Ask/offer price at this point
251    pub ask: Option<f64>,
252    /// Last traded price at this point
253    #[serde(rename = "lastTraded")]
254    pub last_traded: Option<f64>,
255}
256
257/// Information about API usage allowance for price data
258#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
259pub struct PriceAllowance {
260    /// Remaining API calls allowed in the current period
261    #[serde(rename = "remainingAllowance")]
262    pub remaining_allowance: i64,
263    /// Total API calls allowed per period
264    #[serde(rename = "totalAllowance")]
265    pub total_allowance: i64,
266    /// Time until the allowance resets
267    #[serde(rename = "allowanceExpiry")]
268    pub allowance_expiry: i64,
269}
270
271/// Details about instrument expiry
272#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
273pub struct ExpiryDetails {
274    /// The last dealing date and time for the instrument
275    #[serde(rename = "lastDealingDate")]
276    pub last_dealing_date: String,
277
278    /// Information about settlement
279    #[serde(rename = "settlementInfo")]
280    pub settlement_info: Option<String>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
284/// Unit for step distances in trading rules
285pub enum StepUnit {
286    #[serde(rename = "POINTS")]
287    /// Points (price movement units)
288    Points,
289    #[serde(rename = "PERCENTAGE")]
290    /// Percentage value
291    Percentage,
292    #[serde(rename = "pct")]
293    /// Alternative representation for percentage
294    Pct,
295}
296
297/// A struct to handle the minStepDistance value which can be a complex object
298#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
299pub struct StepDistance {
300    /// Unit type for the distance
301    pub unit: Option<StepUnit>,
302    /// Numeric value of the distance
303    pub value: Option<f64>,
304}
305
306/// Node in the market navigation hierarchy
307#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
308pub struct MarketNavigationNode {
309    /// Unique identifier for the node
310    pub id: String,
311    /// Display name of the node
312    pub name: String,
313}
314
315/// Structure representing a node in the market hierarchy
316#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
317pub struct MarketNode {
318    /// Node ID
319    pub id: String,
320    /// Node name
321    pub name: String,
322    /// Child nodes
323    #[serde(skip_serializing_if = "Vec::is_empty", default)]
324    pub children: Vec<MarketNode>,
325    /// Markets in this node
326    #[serde(skip_serializing_if = "Vec::is_empty", default)]
327    pub markets: Vec<MarketData>,
328}
329
330/// Represents the current state of a market
331#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize, Clone, PartialEq, Default)]
332#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
333pub enum MarketState {
334    /// Market is closed for trading
335    Closed,
336    /// Market is offline and not available
337    #[default]
338    Offline,
339    /// Market is open and available for trading
340    Tradeable,
341    /// Market is in edit mode
342    Edit,
343    /// Market is in edit mode only (no new positions, only edits allowed)
344    EditsOnly,
345    /// Market is in auction phase
346    Auction,
347    /// Market is in auction phase but editing is not allowed
348    AuctionNoEdit,
349    /// Market is temporarily suspended
350    Suspended,
351    /// Market is in auction phase
352    OnAuction,
353    /// Market is in auction phase but editing is not allowed
354    OnAuctionNoEdits,
355}
356
357/// Representation of market data received from the IG Markets streaming API
358#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
359pub struct PresentationMarketData {
360    /// Name of the item this data belongs to
361    pub item_name: String,
362    /// Position of the item in the subscription
363    pub item_pos: i32,
364    /// All market fields
365    pub fields: MarketFields,
366    /// Fields that have changed in this update
367    pub changed_fields: MarketFields,
368    /// Whether this is a snapshot or an update
369    pub is_snapshot: bool,
370}
371
372impl PresentationMarketData {
373    /// Converts an ItemUpdate from the Lightstreamer API to a MarketData object
374    ///
375    /// # Arguments
376    /// * `item_update` - The ItemUpdate received from the Lightstreamer API
377    ///
378    /// # Returns
379    /// * `Result<Self, String>` - The converted MarketData or an error message
380    pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
381        // Extract the item_name, defaulting to an empty string if None
382        let item_name = item_update.item_name.clone().unwrap_or_default();
383
384        // Convert item_pos from usize to i32
385        let item_pos = item_update.item_pos as i32;
386
387        // Extract is_snapshot
388        let is_snapshot = item_update.is_snapshot;
389
390        // Convert fields
391        let fields = Self::create_market_fields(&item_update.fields)?;
392
393        // Convert changed_fields by first creating a HashMap<String, Option<String>>
394        let mut changed_fields_map: HashMap<String, Option<String>> = HashMap::new();
395        for (key, value) in &item_update.changed_fields {
396            changed_fields_map.insert(key.clone(), Some(value.clone()));
397        }
398        let changed_fields = Self::create_market_fields(&changed_fields_map)?;
399
400        Ok(PresentationMarketData {
401            item_name,
402            item_pos,
403            fields,
404            changed_fields,
405            is_snapshot,
406        })
407    }
408
409    /// Helper method to create MarketFields from a HashMap of field values
410    ///
411    /// # Arguments
412    /// * `fields_map` - HashMap containing field names and their string values
413    ///
414    /// # Returns
415    /// * `Result<MarketFields, String>` - The parsed MarketFields or an error message
416    fn create_market_fields(
417        fields_map: &HashMap<String, Option<String>>,
418    ) -> Result<MarketFields, String> {
419        // Helper function to safely get a field value
420        let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
421
422        // Parse market state
423        let market_state = match get_field("MARKET_STATE").as_deref() {
424            Some("closed") => Some(MarketState::Closed),
425            Some("offline") => Some(MarketState::Offline),
426            Some("tradeable") => Some(MarketState::Tradeable),
427            Some("edit") => Some(MarketState::Edit),
428            Some("auction") => Some(MarketState::Auction),
429            Some("auction_no_edit") => Some(MarketState::AuctionNoEdit),
430            Some("suspended") => Some(MarketState::Suspended),
431            Some("on_auction") => Some(MarketState::OnAuction),
432            Some("on_auction_no_edit") => Some(MarketState::OnAuctionNoEdits),
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 From<&ItemUpdate> for PresentationMarketData {
472    fn from(item_update: &ItemUpdate) -> Self {
473        Self::from_item_update(item_update).unwrap_or_else(|_| PresentationMarketData {
474            item_name: String::new(),
475            item_pos: 0,
476            fields: MarketFields::default(),
477            changed_fields: MarketFields::default(),
478            is_snapshot: false,
479        })
480    }
481}
482
483/// Fields containing market price and status information
484#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
485pub struct MarketFields {
486    /// The mid-open price of the market
487    #[serde(rename = "MID_OPEN")]
488    #[serde(with = "string_as_float_opt")]
489    #[serde(default)]
490    pub mid_open: Option<f64>,
491
492    /// The highest price reached by the market in the current trading session
493    #[serde(rename = "HIGH")]
494    #[serde(with = "string_as_float_opt")]
495    #[serde(default)]
496    pub high: Option<f64>,
497
498    /// The current offer (ask) price of the market
499    #[serde(rename = "OFFER")]
500    #[serde(with = "string_as_float_opt")]
501    #[serde(default)]
502    pub offer: Option<f64>,
503
504    /// The absolute price change since the previous close
505    #[serde(rename = "CHANGE")]
506    #[serde(with = "string_as_float_opt")]
507    #[serde(default)]
508    pub change: Option<f64>,
509
510    /// Indicates if there is a delay in market data
511    #[serde(rename = "MARKET_DELAY")]
512    #[serde(with = "string_as_bool_opt")]
513    #[serde(default)]
514    pub market_delay: Option<bool>,
515
516    /// The lowest price reached by the market in the current trading session
517    #[serde(rename = "LOW")]
518    #[serde(with = "string_as_float_opt")]
519    #[serde(default)]
520    pub low: Option<f64>,
521
522    /// The current bid price of the market
523    #[serde(rename = "BID")]
524    #[serde(with = "string_as_float_opt")]
525    #[serde(default)]
526    pub bid: Option<f64>,
527
528    /// The percentage price change since the previous close
529    #[serde(rename = "CHANGE_PCT")]
530    #[serde(with = "string_as_float_opt")]
531    #[serde(default)]
532    pub change_pct: Option<f64>,
533
534    /// The current state of the market (e.g., Tradeable, Closed, etc.)
535    #[serde(rename = "MARKET_STATE")]
536    #[serde(default)]
537    pub market_state: Option<MarketState>,
538
539    /// The timestamp of the last market update
540    #[serde(rename = "UPDATE_TIME")]
541    #[serde(default)]
542    pub update_time: Option<String>,
543}