ig_client/model/
responses.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 19/10/25
5******************************************************************************/
6use crate::prelude::{Account, Activity, MarketDetails};
7use crate::presentation::account::{
8    AccountTransaction, ActivityMetadata, Position, TransactionMetadata, WorkingOrder,
9};
10use crate::presentation::instrument::InstrumentType;
11use crate::presentation::market::{
12    HistoricalPrice, MarketData, MarketNavigationNode, MarketNode, PriceAllowance,
13};
14use crate::presentation::order::{Direction, Status};
15use crate::utils::parsing::{deserialize_null_as_empty_vec, deserialize_nullable_status};
16use chrono::{DateTime, Utc};
17use pretty_simple_display::{DebugPretty, DisplaySimple};
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// Database entry response for market instruments
22#[derive(
23    DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default,
24)]
25pub struct DBEntryResponse {
26    /// The trading symbol identifier
27    pub symbol: String,
28    /// The Epic identifier used by the exchange
29    pub epic: String,
30    /// Human-readable name of the instrument
31    pub name: String,
32    /// Instrument type classification
33    pub instrument_type: InstrumentType,
34    /// The exchange where this instrument is traded
35    pub exchange: String,
36    /// Expiration date and time for the instrument
37    pub expiry: String,
38    /// Timestamp of the last update to this record
39    pub last_update: DateTime<Utc>,
40}
41
42impl From<MarketNode> for DBEntryResponse {
43    fn from(value: MarketNode) -> Self {
44        let mut entry = DBEntryResponse::default();
45        if !value.markets.is_empty() {
46            let market = &value.markets[0];
47            entry.symbol = market
48                .epic
49                .split('.')
50                .nth(2)
51                .unwrap_or_default()
52                .to_string();
53            entry.epic = market.epic.clone();
54            entry.name = market.instrument_name.clone();
55            entry.instrument_type = market.instrument_type;
56            entry.exchange = "IG".to_string();
57            entry.expiry = market.expiry.clone();
58            entry.last_update = Utc::now();
59        }
60        entry
61    }
62}
63
64impl From<MarketData> for DBEntryResponse {
65    fn from(market: MarketData) -> Self {
66        DBEntryResponse {
67            symbol: market
68                .epic
69                .split('.')
70                .nth(2)
71                .unwrap_or_default()
72                .to_string(),
73            epic: market.epic.clone(),
74            name: market.instrument_name.clone(),
75            instrument_type: market.instrument_type,
76            exchange: "IG".to_string(),
77            expiry: market.expiry.clone(),
78            last_update: Utc::now(),
79        }
80    }
81}
82
83impl From<&MarketNode> for DBEntryResponse {
84    fn from(value: &MarketNode) -> Self {
85        DBEntryResponse::from(value.clone())
86    }
87}
88
89impl From<&MarketData> for DBEntryResponse {
90    fn from(market: &MarketData) -> Self {
91        DBEntryResponse::from(market.clone())
92    }
93}
94
95/// Response containing multiple market details
96#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
97pub struct MultipleMarketDetailsResponse {
98    /// List of market details
99    #[serde(rename = "marketDetails")]
100    pub market_details: Vec<MarketDetails>,
101}
102
103impl MultipleMarketDetailsResponse {
104    /// Returns the number of market details in the response
105    ///
106    /// # Returns
107    /// Number of market details
108    #[must_use]
109    pub fn len(&self) -> usize {
110        self.market_details.len()
111    }
112
113    /// Returns true if the response contains no market details
114    ///
115    /// # Returns
116    /// True if empty, false otherwise
117    #[must_use]
118    pub fn is_empty(&self) -> bool {
119        self.market_details.is_empty()
120    }
121
122    /// Returns a reference to the market details vector
123    ///
124    /// # Returns
125    /// Reference to the vector of market details
126    #[must_use]
127    pub fn market_details(&self) -> &Vec<MarketDetails> {
128        &self.market_details
129    }
130
131    /// Returns an iterator over the market details
132    ///
133    /// # Returns
134    /// Iterator over market details
135    pub fn iter(&self) -> impl Iterator<Item = &MarketDetails> {
136        self.market_details.iter()
137    }
138}
139
140/// Model for historical prices
141#[derive(DebugPretty, Clone, Serialize, Deserialize)]
142pub struct HistoricalPricesResponse {
143    /// List of historical price points
144    pub prices: Vec<HistoricalPrice>,
145    /// Type of the instrument
146    #[serde(rename = "instrumentType")]
147    pub instrument_type: InstrumentType,
148    /// API usage allowance information
149    #[serde(rename = "allowance", skip_serializing_if = "Option::is_none", default)]
150    pub allowance: Option<PriceAllowance>,
151}
152
153impl HistoricalPricesResponse {
154    /// Returns the number of price points in the response
155    ///
156    /// # Returns
157    /// Number of price points
158    #[must_use]
159    pub fn len(&self) -> usize {
160        self.prices.len()
161    }
162
163    /// Returns true if the response contains no price points
164    ///
165    /// # Returns
166    /// True if empty, false otherwise
167    #[must_use]
168    pub fn is_empty(&self) -> bool {
169        self.prices.is_empty()
170    }
171
172    /// Returns a reference to the prices vector
173    ///
174    /// # Returns
175    /// Reference to the vector of historical prices
176    #[must_use]
177    pub fn prices(&self) -> &Vec<HistoricalPrice> {
178        &self.prices
179    }
180
181    /// Returns an iterator over the prices
182    ///
183    /// # Returns
184    /// Iterator over historical prices
185    pub fn iter(&self) -> impl Iterator<Item = &HistoricalPrice> {
186        self.prices.iter()
187    }
188}
189
190/// Model for market search results
191#[derive(DebugPretty, Clone, Serialize, Deserialize)]
192pub struct MarketSearchResponse {
193    /// List of markets matching the search criteria
194    pub markets: Vec<MarketData>,
195}
196
197impl MarketSearchResponse {
198    /// Returns the number of markets in the response
199    ///
200    /// # Returns
201    /// Number of markets
202    #[must_use]
203    pub fn len(&self) -> usize {
204        self.markets.len()
205    }
206
207    /// Returns true if the response contains no markets
208    ///
209    /// # Returns
210    /// True if empty, false otherwise
211    #[must_use]
212    pub fn is_empty(&self) -> bool {
213        self.markets.is_empty()
214    }
215
216    /// Returns a reference to the markets vector
217    ///
218    /// # Returns
219    /// Reference to the vector of markets
220    #[must_use]
221    pub fn markets(&self) -> &Vec<MarketData> {
222        &self.markets
223    }
224
225    /// Returns an iterator over the markets
226    ///
227    /// # Returns
228    /// Iterator over markets
229    pub fn iter(&self) -> impl Iterator<Item = &MarketData> {
230        self.markets.iter()
231    }
232}
233
234/// Response model for market navigation
235#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
236pub struct MarketNavigationResponse {
237    /// List of navigation nodes at the current level
238    #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
239    pub nodes: Vec<MarketNavigationNode>,
240    /// List of markets at the current level
241    #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
242    pub markets: Vec<MarketData>,
243}
244
245/// Response containing user accounts
246#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
247pub struct AccountsResponse {
248    /// List of accounts owned by the user
249    pub accounts: Vec<Account>,
250}
251
252/// Open positions
253#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
254pub struct PositionsResponse {
255    /// List of open positions
256    pub positions: Vec<Position>,
257}
258
259impl PositionsResponse {
260    /// Compact positions by epic, combining positions with the same epic
261    ///
262    /// This method takes a vector of positions and returns a new vector where
263    /// positions with the same epic have been combined into a single position.
264    ///
265    /// # Arguments
266    /// * `positions` - A vector of positions to compact
267    ///
268    /// # Returns
269    /// A vector of positions with unique epics
270    pub fn compact_by_epic(positions: Vec<Position>) -> Vec<Position> {
271        let mut epic_map: HashMap<String, Position> = std::collections::HashMap::new();
272
273        for position in positions {
274            let epic = position.market.epic.clone();
275            epic_map
276                .entry(epic)
277                .and_modify(|existing| {
278                    *existing = existing.clone() + position.clone();
279                })
280                .or_insert(position);
281        }
282
283        epic_map.into_values().collect()
284    }
285}
286
287/// Working orders
288#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
289pub struct WorkingOrdersResponse {
290    /// List of pending working orders
291    #[serde(rename = "workingOrders")]
292    pub working_orders: Vec<WorkingOrder>,
293}
294
295/// Account activity
296#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
297pub struct AccountActivityResponse {
298    /// List of activities on the account
299    pub activities: Vec<Activity>,
300    /// Metadata about pagination
301    pub metadata: Option<ActivityMetadata>,
302}
303
304/// Transaction history
305#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
306pub struct TransactionHistoryResponse {
307    /// List of account transactions
308    pub transactions: Vec<AccountTransaction>,
309    /// Metadata about the transaction list
310    pub metadata: TransactionMetadata,
311}
312
313/// Response to order creation
314#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
315pub struct CreateOrderResponse {
316    /// Client-generated reference for the deal
317    #[serde(rename = "dealReference")]
318    pub deal_reference: String,
319}
320
321/// Response to closing a position
322#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
323pub struct ClosePositionResponse {
324    /// Client-generated reference for the closing deal
325    #[serde(rename = "dealReference")]
326    pub deal_reference: String,
327}
328
329/// Response to updating a position
330#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
331pub struct UpdatePositionResponse {
332    /// Client-generated reference for the update deal
333    #[serde(rename = "dealReference")]
334    pub deal_reference: String,
335}
336
337/// Response to working order creation
338#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
339pub struct CreateWorkingOrderResponse {
340    /// Client-generated reference for the deal
341    #[serde(rename = "dealReference")]
342    pub deal_reference: String,
343}
344
345/// Details of a confirmed order
346#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
347pub struct OrderConfirmationResponse {
348    /// Date and time of the confirmation
349    pub date: String,
350    /// Status of the order (accepted, rejected, etc.)
351    /// This can be null in some responses (e.g., when market is closed)
352    #[serde(deserialize_with = "deserialize_nullable_status")]
353    pub status: Status,
354    /// Reason for rejection if applicable
355    pub reason: Option<String>,
356    /// Unique identifier for the deal
357    #[serde(rename = "dealId")]
358    pub deal_id: Option<String>,
359    /// Client-generated reference for the deal
360    #[serde(rename = "dealReference")]
361    pub deal_reference: String,
362    /// Status of the deal
363    #[serde(rename = "dealStatus")]
364    pub deal_status: Option<String>,
365    /// Instrument EPIC identifier
366    pub epic: Option<String>,
367    /// Expiry date for the order
368    #[serde(rename = "expiry")]
369    pub expiry: Option<String>,
370    /// Whether a guaranteed stop was used
371    #[serde(rename = "guaranteedStop")]
372    pub guaranteed_stop: Option<bool>,
373    /// Price level of the order
374    #[serde(rename = "level")]
375    pub level: Option<f64>,
376    /// Distance for take profit
377    #[serde(rename = "limitDistance")]
378    pub limit_distance: Option<f64>,
379    /// Price level for take profit
380    #[serde(rename = "limitLevel")]
381    pub limit_level: Option<f64>,
382    /// Size/quantity of the order
383    pub size: Option<f64>,
384    /// Distance for stop loss
385    #[serde(rename = "stopDistance")]
386    pub stop_distance: Option<f64>,
387    /// Price level for stop loss
388    #[serde(rename = "stopLevel")]
389    pub stop_level: Option<f64>,
390    /// Whether a trailing stop was used
391    #[serde(rename = "trailingStop")]
392    pub trailing_stop: Option<bool>,
393    /// Direction of the order (buy or sell)
394    pub direction: Option<Direction>,
395}
396
397impl std::fmt::Display for MultipleMarketDetailsResponse {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        use prettytable::format;
400        use prettytable::{Cell, Row, Table};
401
402        let mut table = Table::new();
403
404        // Set table format
405        table.set_format(*format::consts::FORMAT_BOX_CHARS);
406
407        // Add header
408        table.add_row(Row::new(vec![
409            Cell::new("INSTRUMENT NAME"),
410            Cell::new("EPIC"),
411            Cell::new("BID"),
412            Cell::new("OFFER"),
413            Cell::new("MID"),
414            Cell::new("SPREAD"),
415            Cell::new("EXPIRY"),
416            Cell::new("HIGH/LOW"),
417        ]));
418
419        // Sort by instrument name
420        let mut sorted_details = self.market_details.clone();
421        sorted_details.sort_by(|a, b| {
422            a.instrument
423                .name
424                .to_lowercase()
425                .cmp(&b.instrument.name.to_lowercase())
426        });
427
428        // Add rows
429        for details in &sorted_details {
430            let bid = details
431                .snapshot
432                .bid
433                .map(|b| format!("{:.2}", b))
434                .unwrap_or_else(|| "-".to_string());
435
436            let offer = details
437                .snapshot
438                .offer
439                .map(|o| format!("{:.2}", o))
440                .unwrap_or_else(|| "-".to_string());
441
442            let mid = match (details.snapshot.bid, details.snapshot.offer) {
443                (Some(b), Some(o)) => format!("{:.2}", (b + o) / 2.0),
444                _ => "-".to_string(),
445            };
446
447            let spread = match (details.snapshot.bid, details.snapshot.offer) {
448                (Some(b), Some(o)) => format!("{:.2}", o - b),
449                _ => "-".to_string(),
450            };
451
452            // Use expiry directly (shorter than last_dealing_date)
453            let expiry = details
454                .instrument
455                .expiry_details
456                .as_ref()
457                .map(|ed| {
458                    // Extract just the date part (YYYY-MM-DD)
459                    ed.last_dealing_date
460                        .split('T')
461                        .next()
462                        .unwrap_or(&ed.last_dealing_date)
463                        .to_string()
464                })
465                .unwrap_or_else(|| {
466                    details
467                        .instrument
468                        .expiry
469                        .split('T')
470                        .next()
471                        .unwrap_or(&details.instrument.expiry)
472                        .to_string()
473                });
474
475            let high_low = format!(
476                "{}/{}",
477                details
478                    .snapshot
479                    .high
480                    .map(|h| format!("{:.2}", h))
481                    .unwrap_or_else(|| "-".to_string()),
482                details
483                    .snapshot
484                    .low
485                    .map(|l| format!("{:.2}", l))
486                    .unwrap_or_else(|| "-".to_string())
487            );
488
489            // Truncate long names to make room for EPIC
490            let name = if details.instrument.name.len() > 30 {
491                format!("{}...", &details.instrument.name[0..27])
492            } else {
493                details.instrument.name.clone()
494            };
495
496            // Don't truncate EPIC - show it complete
497            let epic = details.instrument.epic.clone();
498
499            table.add_row(Row::new(vec![
500                Cell::new(&name),
501                Cell::new(&epic),
502                Cell::new(&bid),
503                Cell::new(&offer),
504                Cell::new(&mid),
505                Cell::new(&spread),
506                Cell::new(&expiry),
507                Cell::new(&high_low),
508            ]));
509        }
510
511        write!(f, "{}", table)
512    }
513}
514
515impl std::fmt::Display for HistoricalPricesResponse {
516    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517        use prettytable::format;
518        use prettytable::{Cell, Row, Table};
519
520        let mut table = Table::new();
521        table.set_format(*format::consts::FORMAT_BOX_CHARS);
522
523        // Add header
524        table.add_row(Row::new(vec![
525            Cell::new("SNAPSHOT TIME"),
526            Cell::new("OPEN BID"),
527            Cell::new("OPEN ASK"),
528            Cell::new("HIGH BID"),
529            Cell::new("HIGH ASK"),
530            Cell::new("LOW BID"),
531            Cell::new("LOW ASK"),
532            Cell::new("CLOSE BID"),
533            Cell::new("CLOSE ASK"),
534            Cell::new("VOLUME"),
535        ]));
536
537        // Add rows
538        for price in &self.prices {
539            let open_bid = price
540                .open_price
541                .bid
542                .map(|v| format!("{:.4}", v))
543                .unwrap_or_else(|| "-".to_string());
544
545            let open_ask = price
546                .open_price
547                .ask
548                .map(|v| format!("{:.4}", v))
549                .unwrap_or_else(|| "-".to_string());
550
551            let high_bid = price
552                .high_price
553                .bid
554                .map(|v| format!("{:.4}", v))
555                .unwrap_or_else(|| "-".to_string());
556
557            let high_ask = price
558                .high_price
559                .ask
560                .map(|v| format!("{:.4}", v))
561                .unwrap_or_else(|| "-".to_string());
562
563            let low_bid = price
564                .low_price
565                .bid
566                .map(|v| format!("{:.4}", v))
567                .unwrap_or_else(|| "-".to_string());
568
569            let low_ask = price
570                .low_price
571                .ask
572                .map(|v| format!("{:.4}", v))
573                .unwrap_or_else(|| "-".to_string());
574
575            let close_bid = price
576                .close_price
577                .bid
578                .map(|v| format!("{:.4}", v))
579                .unwrap_or_else(|| "-".to_string());
580
581            let close_ask = price
582                .close_price
583                .ask
584                .map(|v| format!("{:.4}", v))
585                .unwrap_or_else(|| "-".to_string());
586
587            let volume = price
588                .last_traded_volume
589                .map(|v| v.to_string())
590                .unwrap_or_else(|| "-".to_string());
591
592            table.add_row(Row::new(vec![
593                Cell::new(&price.snapshot_time),
594                Cell::new(&open_bid),
595                Cell::new(&open_ask),
596                Cell::new(&high_bid),
597                Cell::new(&high_ask),
598                Cell::new(&low_bid),
599                Cell::new(&low_ask),
600                Cell::new(&close_bid),
601                Cell::new(&close_ask),
602                Cell::new(&volume),
603            ]));
604        }
605
606        // Add summary footer
607        writeln!(f, "{}", table)?;
608        writeln!(f, "\nSummary:")?;
609        writeln!(f, "  Total price points: {}", self.prices.len())?;
610        writeln!(f, "  Instrument type: {:?}", self.instrument_type)?;
611
612        if let Some(allowance) = &self.allowance {
613            writeln!(
614                f,
615                "  Remaining allowance: {}",
616                allowance.remaining_allowance
617            )?;
618            writeln!(f, "  Total allowance: {}", allowance.total_allowance)?;
619        }
620
621        Ok(())
622    }
623}
624
625impl std::fmt::Display for MarketSearchResponse {
626    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
627        use prettytable::format;
628        use prettytable::{Cell, Row, Table};
629
630        let mut table = Table::new();
631        table.set_format(*format::consts::FORMAT_BOX_CHARS);
632
633        // Add header
634        table.add_row(Row::new(vec![
635            Cell::new("INSTRUMENT NAME"),
636            Cell::new("EPIC"),
637            Cell::new("BID"),
638            Cell::new("OFFER"),
639            Cell::new("MID"),
640            Cell::new("SPREAD"),
641            Cell::new("EXPIRY"),
642            Cell::new("TYPE"),
643        ]));
644
645        // Sort by instrument name
646        let mut sorted_markets = self.markets.clone();
647        sorted_markets.sort_by(|a, b| {
648            a.instrument_name
649                .to_lowercase()
650                .cmp(&b.instrument_name.to_lowercase())
651        });
652
653        // Add rows
654        for market in &sorted_markets {
655            let bid = market
656                .bid
657                .map(|b| format!("{:.4}", b))
658                .unwrap_or_else(|| "-".to_string());
659
660            let offer = market
661                .offer
662                .map(|o| format!("{:.4}", o))
663                .unwrap_or_else(|| "-".to_string());
664
665            let mid = match (market.bid, market.offer) {
666                (Some(b), Some(o)) => format!("{:.4}", (b + o) / 2.0),
667                _ => "-".to_string(),
668            };
669
670            let spread = match (market.bid, market.offer) {
671                (Some(b), Some(o)) => format!("{:.4}", o - b),
672                _ => "-".to_string(),
673            };
674
675            // Truncate long names
676            let name = if market.instrument_name.len() > 30 {
677                format!("{}...", &market.instrument_name[0..27])
678            } else {
679                market.instrument_name.clone()
680            };
681
682            // Extract date from expiry
683            let expiry = market
684                .expiry
685                .split('T')
686                .next()
687                .unwrap_or(&market.expiry)
688                .to_string();
689
690            let instrument_type = format!("{:?}", market.instrument_type);
691
692            table.add_row(Row::new(vec![
693                Cell::new(&name),
694                Cell::new(&market.epic),
695                Cell::new(&bid),
696                Cell::new(&offer),
697                Cell::new(&mid),
698                Cell::new(&spread),
699                Cell::new(&expiry),
700                Cell::new(&instrument_type),
701            ]));
702        }
703
704        writeln!(f, "{}", table)?;
705        writeln!(f, "\nTotal markets found: {}", self.markets.len())?;
706
707        Ok(())
708    }
709}
710
711#[cfg(test)]
712mod tests {
713    use super::*;
714    use serde_json::Value;
715    use std::fs;
716
717    #[test]
718    fn test_deserialize_working_orders_from_file() {
719        // Load the JSON file
720        let json_content = fs::read_to_string("Data/working_orders.json")
721            .expect("Failed to read Data/working_orders.json");
722
723        // Parse as a generic JSON Value first to inspect the structure
724        let json_value: Value =
725            serde_json::from_str(&json_content).expect("Failed to parse JSON as Value");
726
727        println!(
728            "JSON structure:\n{}",
729            serde_json::to_string_pretty(&json_value).unwrap()
730        );
731
732        // Attempt to deserialize into WorkingOrdersResponse
733        let result: Result<WorkingOrdersResponse, _> = serde_json::from_str(&json_content);
734
735        match result {
736            Ok(response) => {
737                println!(
738                    "Successfully deserialized {} working orders",
739                    response.working_orders.len()
740                );
741                for (idx, order) in response.working_orders.iter().enumerate() {
742                    println!(
743                        "Order {}: epic={}, direction={:?}, size={}, level={}",
744                        idx + 1,
745                        order.working_order_data.epic,
746                        order.working_order_data.direction,
747                        order.working_order_data.order_size,
748                        order.working_order_data.order_level
749                    );
750                }
751            }
752            Err(e) => {
753                panic!(
754                    "Failed to deserialize WorkingOrdersResponse: {}\n\nJSON was:\n{}",
755                    e,
756                    serde_json::to_string_pretty(&json_value).unwrap()
757                );
758            }
759        }
760    }
761}