1use crate::presentation::instrument::InstrumentType;
2use crate::presentation::serialization::{string_as_bool_opt, string_as_float_opt};
3use lightstreamer_rs::subscription::ItemUpdate;
4use pretty_simple_display::{DebugPretty, DisplaySimple};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
10pub struct Instrument {
11 pub epic: String,
13 pub name: String,
15 pub expiry: String,
17 #[serde(rename = "contractSize")]
19 pub contract_size: String,
20 #[serde(rename = "lotSize")]
22 pub lot_size: Option<f64>,
23 #[serde(rename = "highLimitPrice")]
25 pub high_limit_price: Option<f64>,
26 #[serde(rename = "lowLimitPrice")]
28 pub low_limit_price: Option<f64>,
29 #[serde(rename = "marginFactor")]
31 pub margin_factor: Option<f64>,
32 #[serde(rename = "marginFactorUnit")]
34 pub margin_factor_unit: Option<String>,
35 pub currencies: Option<Vec<Currency>>,
37 #[serde(rename = "valueOfOnePip")]
38 pub value_of_one_pip: String,
40 #[serde(rename = "instrumentType")]
42 pub instrument_type: Option<InstrumentType>,
43 #[serde(rename = "expiryDetails")]
45 pub expiry_details: Option<ExpiryDetails>,
46 #[serde(rename = "slippageFactor")]
47 pub slippage_factor: Option<StepDistance>,
49 #[serde(rename = "limitedRiskPremium")]
50 pub limited_risk_premium: Option<StepDistance>,
52 #[serde(rename = "newsCode")]
53 pub news_code: Option<String>,
55 #[serde(rename = "chartCode")]
56 pub chart_code: Option<String>,
58}
59
60#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
62pub struct Currency {
63 pub code: String,
65 pub symbol: Option<String>,
67 #[serde(rename = "baseExchangeRate")]
69 pub base_exchange_rate: Option<f64>,
70 #[serde(rename = "exchangeRate")]
72 pub exchange_rate: Option<f64>,
73 #[serde(rename = "isDefault")]
75 pub is_default: Option<bool>,
76}
77
78#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
80pub struct MarketDetails {
81 pub instrument: Instrument,
83 pub snapshot: MarketSnapshot,
85 #[serde(rename = "dealingRules")]
87 pub dealing_rules: DealingRules,
88}
89
90#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
92pub struct DealingRules {
93 #[serde(rename = "minStepDistance")]
95 pub min_step_distance: StepDistance,
96
97 #[serde(rename = "minDealSize")]
99 pub min_deal_size: StepDistance,
100
101 #[serde(rename = "minControlledRiskStopDistance")]
103 pub min_controlled_risk_stop_distance: StepDistance,
104
105 #[serde(rename = "minNormalStopOrLimitDistance")]
107 pub min_normal_stop_or_limit_distance: StepDistance,
108
109 #[serde(rename = "maxStopOrLimitDistance")]
111 pub max_stop_or_limit_distance: StepDistance,
112
113 #[serde(rename = "controlledRiskSpacing")]
115 pub controlled_risk_spacing: StepDistance,
116
117 #[serde(rename = "marketOrderPreference")]
119 pub market_order_preference: String,
120
121 #[serde(rename = "trailingStopsPreference")]
123 pub trailing_stops_preference: String,
124
125 #[serde(rename = "maxDealSize")]
126 pub max_deal_size: Option<f64>,
128}
129
130#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
132pub struct MarketSnapshot {
133 #[serde(rename = "marketStatus")]
135 pub market_status: String,
136
137 #[serde(rename = "netChange")]
139 pub net_change: Option<f64>,
140
141 #[serde(rename = "percentageChange")]
143 pub percentage_change: Option<f64>,
144
145 #[serde(rename = "updateTime")]
147 pub update_time: Option<String>,
148
149 #[serde(rename = "delayTime")]
151 pub delay_time: Option<i64>,
152
153 pub bid: Option<f64>,
155
156 pub offer: Option<f64>,
158
159 pub high: Option<f64>,
161
162 pub low: Option<f64>,
164
165 #[serde(rename = "binaryOdds")]
167 pub binary_odds: Option<f64>,
168
169 #[serde(rename = "decimalPlacesFactor")]
171 pub decimal_places_factor: Option<i64>,
172
173 #[serde(rename = "scalingFactor")]
175 pub scaling_factor: Option<i64>,
176
177 #[serde(rename = "controlledRiskExtraSpread")]
179 pub controlled_risk_extra_spread: Option<f64>,
180}
181
182#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
184pub struct MarketData {
185 pub epic: String,
187 #[serde(rename = "instrumentName")]
189 pub instrument_name: String,
190 #[serde(rename = "instrumentType")]
192 pub instrument_type: InstrumentType,
193 pub expiry: String,
195 #[serde(rename = "highLimitPrice")]
197 pub high_limit_price: Option<f64>,
198 #[serde(rename = "lowLimitPrice")]
200 pub low_limit_price: Option<f64>,
201 #[serde(rename = "marketStatus")]
203 pub market_status: String,
204 #[serde(rename = "netChange")]
206 pub net_change: Option<f64>,
207 #[serde(rename = "percentageChange")]
209 pub percentage_change: Option<f64>,
210 #[serde(rename = "updateTime")]
212 pub update_time: Option<String>,
213 #[serde(rename = "updateTimeUTC")]
215 pub update_time_utc: Option<String>,
216 pub bid: Option<f64>,
218 pub offer: Option<f64>,
220}
221
222impl MarketData {
223 pub fn is_call(&self) -> bool {
236 self.instrument_name.contains("CALL")
237 }
238
239 pub fn is_put(&self) -> bool {
251 self.instrument_name.contains("PUT")
252 }
253}
254
255#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
257pub struct HistoricalPrice {
258 #[serde(rename = "snapshotTime")]
260 pub snapshot_time: String,
261 #[serde(rename = "openPrice")]
263 pub open_price: PricePoint,
264 #[serde(rename = "highPrice")]
266 pub high_price: PricePoint,
267 #[serde(rename = "lowPrice")]
269 pub low_price: PricePoint,
270 #[serde(rename = "closePrice")]
272 pub close_price: PricePoint,
273 #[serde(rename = "lastTradedVolume")]
275 pub last_traded_volume: Option<i64>,
276}
277
278#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
280pub struct PricePoint {
281 pub bid: Option<f64>,
283 pub ask: Option<f64>,
285 #[serde(rename = "lastTraded")]
287 pub last_traded: Option<f64>,
288}
289
290#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
292pub struct PriceAllowance {
293 #[serde(rename = "remainingAllowance")]
295 pub remaining_allowance: i64,
296 #[serde(rename = "totalAllowance")]
298 pub total_allowance: i64,
299 #[serde(rename = "allowanceExpiry")]
301 pub allowance_expiry: i64,
302}
303
304#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
306pub struct ExpiryDetails {
307 #[serde(rename = "lastDealingDate")]
309 pub last_dealing_date: String,
310
311 #[serde(rename = "settlementInfo")]
313 pub settlement_info: Option<String>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
317pub enum StepUnit {
319 #[serde(rename = "POINTS")]
320 Points,
322 #[serde(rename = "PERCENTAGE")]
323 Percentage,
325 #[serde(rename = "pct")]
326 Pct,
328}
329
330#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq)]
332pub struct StepDistance {
333 pub unit: Option<StepUnit>,
335 pub value: Option<f64>,
337}
338
339#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
341pub struct MarketNavigationNode {
342 pub id: String,
344 pub name: String,
346}
347
348#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
350pub struct MarketNode {
351 pub id: String,
353 pub name: String,
355 #[serde(skip_serializing_if = "Vec::is_empty", default)]
357 pub children: Vec<MarketNode>,
358 #[serde(skip_serializing_if = "Vec::is_empty", default)]
360 pub markets: Vec<MarketData>,
361}
362
363#[derive(DebugPretty, DisplaySimple, Serialize, Deserialize, Clone, PartialEq, Default)]
365#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
366pub enum MarketState {
367 Closed,
369 #[default]
371 Offline,
372 Tradeable,
374 Edit,
376 EditsOnly,
378 Auction,
380 AuctionNoEdit,
382 Suspended,
384 OnAuction,
386 OnAuctionNoEdits,
388}
389
390#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
392pub struct PresentationMarketData {
393 pub item_name: String,
395 pub item_pos: i32,
397 pub fields: MarketFields,
399 pub changed_fields: MarketFields,
401 pub is_snapshot: bool,
403}
404
405impl PresentationMarketData {
406 pub fn from_item_update(item_update: &ItemUpdate) -> Result<Self, String> {
414 let item_name = item_update.item_name.clone().unwrap_or_default();
416
417 let item_pos = item_update.item_pos as i32;
419
420 let is_snapshot = item_update.is_snapshot;
422
423 let fields = Self::create_market_fields(&item_update.fields)?;
425
426 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 fn create_market_fields(
450 fields_map: &HashMap<String, Option<String>>,
451 ) -> Result<MarketFields, String> {
452 let get_field = |key: &str| -> Option<String> { fields_map.get(key).cloned().flatten() };
454
455 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 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 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#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
518pub struct Category {
519 pub code: String,
521 #[serde(rename = "nonTradeable")]
523 pub non_tradeable: bool,
524}
525
526#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
528#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
529pub enum CategoryMarketStatus {
530 #[default]
532 Offline,
533 Closed,
535 Suspended,
537 OnAuction,
539 OnAuctionNoEdits,
541 EditsOnly,
543 ClosingsOnly,
545 DealNoEdit,
547 Tradeable,
549}
550
551#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
553pub struct CategoryInstrument {
554 pub epic: String,
556 #[serde(rename = "instrumentName")]
558 pub instrument_name: String,
559 pub expiry: String,
561 #[serde(rename = "instrumentType")]
563 pub instrument_type: InstrumentType,
564 #[serde(rename = "lotSize")]
566 pub lot_size: Option<f64>,
567 #[serde(rename = "otcTradeable")]
569 pub otc_tradeable: bool,
570 #[serde(rename = "marketStatus")]
572 pub market_status: CategoryMarketStatus,
573 #[serde(rename = "delayTime")]
575 pub delay_time: Option<i64>,
576 pub bid: Option<f64>,
578 pub offer: Option<f64>,
580 pub high: Option<f64>,
582 pub low: Option<f64>,
584 #[serde(rename = "netChange")]
586 pub net_change: Option<f64>,
587 #[serde(rename = "percentageChange")]
589 pub percentage_change: Option<f64>,
590 #[serde(rename = "updateTime")]
592 pub update_time: Option<String>,
593 #[serde(rename = "scalingFactor")]
595 pub scaling_factor: Option<i64>,
596}
597
598#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
600pub struct CategoryInstrumentsMetadata {
601 #[serde(rename = "pageNumber")]
603 pub page_number: i32,
604 #[serde(rename = "pageSize")]
606 pub page_size: i32,
607}
608
609#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default, PartialEq)]
611pub struct MarketFields {
612 #[serde(rename = "MID_OPEN")]
614 #[serde(with = "string_as_float_opt")]
615 #[serde(default)]
616 pub mid_open: Option<f64>,
617
618 #[serde(rename = "HIGH")]
620 #[serde(with = "string_as_float_opt")]
621 #[serde(default)]
622 pub high: Option<f64>,
623
624 #[serde(rename = "OFFER")]
626 #[serde(with = "string_as_float_opt")]
627 #[serde(default)]
628 pub offer: Option<f64>,
629
630 #[serde(rename = "CHANGE")]
632 #[serde(with = "string_as_float_opt")]
633 #[serde(default)]
634 pub change: Option<f64>,
635
636 #[serde(rename = "MARKET_DELAY")]
638 #[serde(with = "string_as_bool_opt")]
639 #[serde(default)]
640 pub market_delay: Option<bool>,
641
642 #[serde(rename = "LOW")]
644 #[serde(with = "string_as_float_opt")]
645 #[serde(default)]
646 pub low: Option<f64>,
647
648 #[serde(rename = "BID")]
650 #[serde(with = "string_as_float_opt")]
651 #[serde(default)]
652 pub bid: Option<f64>,
653
654 #[serde(rename = "CHANGE_PCT")]
656 #[serde(with = "string_as_float_opt")]
657 #[serde(default)]
658 pub change_pct: Option<f64>,
659
660 #[serde(rename = "MARKET_STATE")]
662 #[serde(default)]
663 pub market_state: Option<MarketState>,
664
665 #[serde(rename = "UPDATE_TIME")]
667 #[serde(default)]
668 pub update_time: Option<String>,
669}