1use 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 Category, CategoryInstrument, CategoryInstrumentsMetadata, HistoricalPrice, MarketData,
13 MarketNavigationNode, MarketNode, PriceAllowance,
14};
15use crate::presentation::order::{Direction, Status};
16use crate::utils::parsing::{deserialize_null_as_empty_vec, deserialize_nullable_status};
17use chrono::{DateTime, Utc};
18use pretty_simple_display::{DebugPretty, DisplaySimple};
19use serde::{Deserialize, Serialize};
20use std::collections::HashMap;
21
22#[derive(
24 DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default,
25)]
26pub struct DBEntryResponse {
27 pub symbol: String,
29 pub epic: String,
31 pub name: String,
33 pub instrument_type: InstrumentType,
35 pub exchange: String,
37 pub expiry: String,
39 pub last_update: DateTime<Utc>,
41}
42
43impl From<MarketNode> for DBEntryResponse {
44 fn from(value: MarketNode) -> Self {
45 let mut entry = DBEntryResponse::default();
46 if !value.markets.is_empty() {
47 let market = &value.markets[0];
48 entry.symbol = market
49 .epic
50 .split('.')
51 .nth(2)
52 .unwrap_or_default()
53 .to_string();
54 entry.epic = market.epic.clone();
55 entry.name = market.instrument_name.clone();
56 entry.instrument_type = market.instrument_type;
57 entry.exchange = "IG".to_string();
58 entry.expiry = market.expiry.clone();
59 entry.last_update = Utc::now();
60 }
61 entry
62 }
63}
64
65impl From<MarketData> for DBEntryResponse {
66 fn from(market: MarketData) -> Self {
67 DBEntryResponse {
68 symbol: market
69 .epic
70 .split('.')
71 .nth(2)
72 .unwrap_or_default()
73 .to_string(),
74 epic: market.epic.clone(),
75 name: market.instrument_name.clone(),
76 instrument_type: market.instrument_type,
77 exchange: "IG".to_string(),
78 expiry: market.expiry.clone(),
79 last_update: Utc::now(),
80 }
81 }
82}
83
84impl From<&MarketNode> for DBEntryResponse {
85 fn from(value: &MarketNode) -> Self {
86 DBEntryResponse::from(value.clone())
87 }
88}
89
90impl From<&MarketData> for DBEntryResponse {
91 fn from(market: &MarketData) -> Self {
92 DBEntryResponse::from(market.clone())
93 }
94}
95
96#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
98pub struct MultipleMarketDetailsResponse {
99 #[serde(rename = "marketDetails")]
101 pub market_details: Vec<MarketDetails>,
102}
103
104impl MultipleMarketDetailsResponse {
105 #[must_use]
110 pub fn len(&self) -> usize {
111 self.market_details.len()
112 }
113
114 #[must_use]
119 pub fn is_empty(&self) -> bool {
120 self.market_details.is_empty()
121 }
122
123 #[must_use]
128 pub fn market_details(&self) -> &Vec<MarketDetails> {
129 &self.market_details
130 }
131
132 pub fn iter(&self) -> impl Iterator<Item = &MarketDetails> {
137 self.market_details.iter()
138 }
139}
140
141#[derive(DebugPretty, Clone, Serialize, Deserialize)]
143pub struct HistoricalPricesResponse {
144 pub prices: Vec<HistoricalPrice>,
146 #[serde(rename = "instrumentType")]
148 pub instrument_type: InstrumentType,
149 #[serde(rename = "allowance", skip_serializing_if = "Option::is_none", default)]
151 pub allowance: Option<PriceAllowance>,
152}
153
154impl HistoricalPricesResponse {
155 #[must_use]
160 pub fn len(&self) -> usize {
161 self.prices.len()
162 }
163
164 #[must_use]
169 pub fn is_empty(&self) -> bool {
170 self.prices.is_empty()
171 }
172
173 #[must_use]
178 pub fn prices(&self) -> &Vec<HistoricalPrice> {
179 &self.prices
180 }
181
182 pub fn iter(&self) -> impl Iterator<Item = &HistoricalPrice> {
187 self.prices.iter()
188 }
189}
190
191#[derive(DebugPretty, Clone, Serialize, Deserialize)]
193pub struct MarketSearchResponse {
194 pub markets: Vec<MarketData>,
196}
197
198impl MarketSearchResponse {
199 #[must_use]
204 pub fn len(&self) -> usize {
205 self.markets.len()
206 }
207
208 #[must_use]
213 pub fn is_empty(&self) -> bool {
214 self.markets.is_empty()
215 }
216
217 #[must_use]
222 pub fn markets(&self) -> &Vec<MarketData> {
223 &self.markets
224 }
225
226 pub fn iter(&self) -> impl Iterator<Item = &MarketData> {
231 self.markets.iter()
232 }
233}
234
235#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
237pub struct MarketNavigationResponse {
238 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
240 pub nodes: Vec<MarketNavigationNode>,
241 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
243 pub markets: Vec<MarketData>,
244}
245
246#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
248pub struct CategoriesResponse {
249 pub categories: Vec<Category>,
251}
252
253impl CategoriesResponse {
254 #[must_use]
259 pub fn len(&self) -> usize {
260 self.categories.len()
261 }
262
263 #[must_use]
268 pub fn is_empty(&self) -> bool {
269 self.categories.is_empty()
270 }
271
272 #[must_use]
277 pub fn categories(&self) -> &Vec<Category> {
278 &self.categories
279 }
280
281 pub fn iter(&self) -> impl Iterator<Item = &Category> {
286 self.categories.iter()
287 }
288}
289
290#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
292pub struct CategoryInstrumentsResponse {
293 pub instruments: Vec<CategoryInstrument>,
295 pub metadata: Option<CategoryInstrumentsMetadata>,
297}
298
299impl CategoryInstrumentsResponse {
300 #[must_use]
305 pub fn len(&self) -> usize {
306 self.instruments.len()
307 }
308
309 #[must_use]
314 pub fn is_empty(&self) -> bool {
315 self.instruments.is_empty()
316 }
317
318 #[must_use]
323 pub fn instruments(&self) -> &Vec<CategoryInstrument> {
324 &self.instruments
325 }
326
327 pub fn iter(&self) -> impl Iterator<Item = &CategoryInstrument> {
332 self.instruments.iter()
333 }
334}
335
336#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
338pub struct AccountsResponse {
339 pub accounts: Vec<Account>,
341}
342
343#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
345pub struct PositionsResponse {
346 pub positions: Vec<Position>,
348}
349
350impl PositionsResponse {
351 pub fn compact_by_epic(positions: Vec<Position>) -> Vec<Position> {
362 let mut epic_map: HashMap<String, Position> = std::collections::HashMap::new();
363
364 for position in positions {
365 let epic = position.market.epic.clone();
366 epic_map
367 .entry(epic)
368 .and_modify(|existing| {
369 *existing = existing.clone() + position.clone();
370 })
371 .or_insert(position);
372 }
373
374 epic_map.into_values().collect()
375 }
376}
377
378#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
380pub struct WorkingOrdersResponse {
381 #[serde(rename = "workingOrders")]
383 pub working_orders: Vec<WorkingOrder>,
384}
385
386#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
388pub struct AccountActivityResponse {
389 pub activities: Vec<Activity>,
391 pub metadata: Option<ActivityMetadata>,
393}
394
395#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
397pub struct TransactionHistoryResponse {
398 pub transactions: Vec<AccountTransaction>,
400 pub metadata: TransactionMetadata,
402}
403
404#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
406pub struct CreateOrderResponse {
407 #[serde(rename = "dealReference")]
409 pub deal_reference: String,
410}
411
412#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
414pub struct ClosePositionResponse {
415 #[serde(rename = "dealReference")]
417 pub deal_reference: String,
418}
419
420#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
422pub struct UpdatePositionResponse {
423 #[serde(rename = "dealReference")]
425 pub deal_reference: String,
426}
427
428#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
430pub struct CreateWorkingOrderResponse {
431 #[serde(rename = "dealReference")]
433 pub deal_reference: String,
434}
435
436#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
438pub struct OrderConfirmationResponse {
439 pub date: String,
441 #[serde(deserialize_with = "deserialize_nullable_status")]
444 pub status: Status,
445 pub reason: Option<String>,
447 #[serde(rename = "dealId")]
449 pub deal_id: Option<String>,
450 #[serde(rename = "dealReference")]
452 pub deal_reference: String,
453 #[serde(rename = "dealStatus")]
455 pub deal_status: Option<String>,
456 pub epic: Option<String>,
458 #[serde(rename = "expiry")]
460 pub expiry: Option<String>,
461 #[serde(rename = "guaranteedStop")]
463 pub guaranteed_stop: Option<bool>,
464 #[serde(rename = "level")]
466 pub level: Option<f64>,
467 #[serde(rename = "limitDistance")]
469 pub limit_distance: Option<f64>,
470 #[serde(rename = "limitLevel")]
472 pub limit_level: Option<f64>,
473 pub size: Option<f64>,
475 #[serde(rename = "stopDistance")]
477 pub stop_distance: Option<f64>,
478 #[serde(rename = "stopLevel")]
480 pub stop_level: Option<f64>,
481 #[serde(rename = "trailingStop")]
483 pub trailing_stop: Option<bool>,
484 pub direction: Option<Direction>,
486}
487
488impl std::fmt::Display for MultipleMarketDetailsResponse {
489 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490 use prettytable::format;
491 use prettytable::{Cell, Row, Table};
492
493 let mut table = Table::new();
494
495 table.set_format(*format::consts::FORMAT_BOX_CHARS);
497
498 table.add_row(Row::new(vec![
500 Cell::new("INSTRUMENT 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 let mut sorted_details = self.market_details.clone();
512 sorted_details.sort_by(|a, b| {
513 a.instrument
514 .name
515 .to_lowercase()
516 .cmp(&b.instrument.name.to_lowercase())
517 });
518
519 for details in &sorted_details {
521 let bid = details
522 .snapshot
523 .bid
524 .map(|b| format!("{:.2}", b))
525 .unwrap_or_else(|| "-".to_string());
526
527 let offer = details
528 .snapshot
529 .offer
530 .map(|o| format!("{:.2}", o))
531 .unwrap_or_else(|| "-".to_string());
532
533 let mid = match (details.snapshot.bid, details.snapshot.offer) {
534 (Some(b), Some(o)) => format!("{:.2}", (b + o) / 2.0),
535 _ => "-".to_string(),
536 };
537
538 let spread = match (details.snapshot.bid, details.snapshot.offer) {
539 (Some(b), Some(o)) => format!("{:.2}", o - b),
540 _ => "-".to_string(),
541 };
542
543 let expiry = details
545 .instrument
546 .expiry_details
547 .as_ref()
548 .map(|ed| {
549 ed.last_dealing_date
551 .split('T')
552 .next()
553 .unwrap_or(&ed.last_dealing_date)
554 .to_string()
555 })
556 .unwrap_or_else(|| {
557 details
558 .instrument
559 .expiry
560 .split('T')
561 .next()
562 .unwrap_or(&details.instrument.expiry)
563 .to_string()
564 });
565
566 let high_low = format!(
567 "{}/{}",
568 details
569 .snapshot
570 .high
571 .map(|h| format!("{:.2}", h))
572 .unwrap_or_else(|| "-".to_string()),
573 details
574 .snapshot
575 .low
576 .map(|l| format!("{:.2}", l))
577 .unwrap_or_else(|| "-".to_string())
578 );
579
580 let name = if details.instrument.name.len() > 30 {
582 format!("{}...", &details.instrument.name[0..27])
583 } else {
584 details.instrument.name.clone()
585 };
586
587 let epic = details.instrument.epic.clone();
589
590 table.add_row(Row::new(vec![
591 Cell::new(&name),
592 Cell::new(&epic),
593 Cell::new(&bid),
594 Cell::new(&offer),
595 Cell::new(&mid),
596 Cell::new(&spread),
597 Cell::new(&expiry),
598 Cell::new(&high_low),
599 ]));
600 }
601
602 write!(f, "{}", table)
603 }
604}
605
606impl std::fmt::Display for HistoricalPricesResponse {
607 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 use prettytable::format;
609 use prettytable::{Cell, Row, Table};
610
611 let mut table = Table::new();
612 table.set_format(*format::consts::FORMAT_BOX_CHARS);
613
614 table.add_row(Row::new(vec![
616 Cell::new("SNAPSHOT TIME"),
617 Cell::new("OPEN BID"),
618 Cell::new("OPEN ASK"),
619 Cell::new("HIGH BID"),
620 Cell::new("HIGH ASK"),
621 Cell::new("LOW BID"),
622 Cell::new("LOW ASK"),
623 Cell::new("CLOSE BID"),
624 Cell::new("CLOSE ASK"),
625 Cell::new("VOLUME"),
626 ]));
627
628 for price in &self.prices {
630 let open_bid = price
631 .open_price
632 .bid
633 .map(|v| format!("{:.4}", v))
634 .unwrap_or_else(|| "-".to_string());
635
636 let open_ask = price
637 .open_price
638 .ask
639 .map(|v| format!("{:.4}", v))
640 .unwrap_or_else(|| "-".to_string());
641
642 let high_bid = price
643 .high_price
644 .bid
645 .map(|v| format!("{:.4}", v))
646 .unwrap_or_else(|| "-".to_string());
647
648 let high_ask = price
649 .high_price
650 .ask
651 .map(|v| format!("{:.4}", v))
652 .unwrap_or_else(|| "-".to_string());
653
654 let low_bid = price
655 .low_price
656 .bid
657 .map(|v| format!("{:.4}", v))
658 .unwrap_or_else(|| "-".to_string());
659
660 let low_ask = price
661 .low_price
662 .ask
663 .map(|v| format!("{:.4}", v))
664 .unwrap_or_else(|| "-".to_string());
665
666 let close_bid = price
667 .close_price
668 .bid
669 .map(|v| format!("{:.4}", v))
670 .unwrap_or_else(|| "-".to_string());
671
672 let close_ask = price
673 .close_price
674 .ask
675 .map(|v| format!("{:.4}", v))
676 .unwrap_or_else(|| "-".to_string());
677
678 let volume = price
679 .last_traded_volume
680 .map(|v| v.to_string())
681 .unwrap_or_else(|| "-".to_string());
682
683 table.add_row(Row::new(vec![
684 Cell::new(&price.snapshot_time),
685 Cell::new(&open_bid),
686 Cell::new(&open_ask),
687 Cell::new(&high_bid),
688 Cell::new(&high_ask),
689 Cell::new(&low_bid),
690 Cell::new(&low_ask),
691 Cell::new(&close_bid),
692 Cell::new(&close_ask),
693 Cell::new(&volume),
694 ]));
695 }
696
697 writeln!(f, "{}", table)?;
699 writeln!(f, "\nSummary:")?;
700 writeln!(f, " Total price points: {}", self.prices.len())?;
701 writeln!(f, " Instrument type: {:?}", self.instrument_type)?;
702
703 if let Some(allowance) = &self.allowance {
704 writeln!(
705 f,
706 " Remaining allowance: {}",
707 allowance.remaining_allowance
708 )?;
709 writeln!(f, " Total allowance: {}", allowance.total_allowance)?;
710 }
711
712 Ok(())
713 }
714}
715
716impl std::fmt::Display for MarketSearchResponse {
717 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718 use prettytable::format;
719 use prettytable::{Cell, Row, Table};
720
721 let mut table = Table::new();
722 table.set_format(*format::consts::FORMAT_BOX_CHARS);
723
724 table.add_row(Row::new(vec![
726 Cell::new("INSTRUMENT NAME"),
727 Cell::new("EPIC"),
728 Cell::new("BID"),
729 Cell::new("OFFER"),
730 Cell::new("MID"),
731 Cell::new("SPREAD"),
732 Cell::new("EXPIRY"),
733 Cell::new("TYPE"),
734 ]));
735
736 let mut sorted_markets = self.markets.clone();
738 sorted_markets.sort_by(|a, b| {
739 a.instrument_name
740 .to_lowercase()
741 .cmp(&b.instrument_name.to_lowercase())
742 });
743
744 for market in &sorted_markets {
746 let bid = market
747 .bid
748 .map(|b| format!("{:.4}", b))
749 .unwrap_or_else(|| "-".to_string());
750
751 let offer = market
752 .offer
753 .map(|o| format!("{:.4}", o))
754 .unwrap_or_else(|| "-".to_string());
755
756 let mid = match (market.bid, market.offer) {
757 (Some(b), Some(o)) => format!("{:.4}", (b + o) / 2.0),
758 _ => "-".to_string(),
759 };
760
761 let spread = match (market.bid, market.offer) {
762 (Some(b), Some(o)) => format!("{:.4}", o - b),
763 _ => "-".to_string(),
764 };
765
766 let name = if market.instrument_name.len() > 30 {
768 format!("{}...", &market.instrument_name[0..27])
769 } else {
770 market.instrument_name.clone()
771 };
772
773 let expiry = market
775 .expiry
776 .split('T')
777 .next()
778 .unwrap_or(&market.expiry)
779 .to_string();
780
781 let instrument_type = format!("{:?}", market.instrument_type);
782
783 table.add_row(Row::new(vec![
784 Cell::new(&name),
785 Cell::new(&market.epic),
786 Cell::new(&bid),
787 Cell::new(&offer),
788 Cell::new(&mid),
789 Cell::new(&spread),
790 Cell::new(&expiry),
791 Cell::new(&instrument_type),
792 ]));
793 }
794
795 writeln!(f, "{}", table)?;
796 writeln!(f, "\nTotal markets found: {}", self.markets.len())?;
797
798 Ok(())
799 }
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805 use serde_json::Value;
806 use std::fs;
807
808 #[test]
809 fn test_deserialize_working_orders_from_file() {
810 let json_content = fs::read_to_string("Data/working_orders.json")
812 .expect("Failed to read Data/working_orders.json");
813
814 let json_value: Value =
816 serde_json::from_str(&json_content).expect("Failed to parse JSON as Value");
817
818 println!(
819 "JSON structure:\n{}",
820 serde_json::to_string_pretty(&json_value).unwrap()
821 );
822
823 let result: Result<WorkingOrdersResponse, _> = serde_json::from_str(&json_content);
825
826 match result {
827 Ok(response) => {
828 println!(
829 "Successfully deserialized {} working orders",
830 response.working_orders.len()
831 );
832 for (idx, order) in response.working_orders.iter().enumerate() {
833 println!(
834 "Order {}: epic={}, direction={:?}, size={}, level={}",
835 idx + 1,
836 order.working_order_data.epic,
837 order.working_order_data.direction,
838 order.working_order_data.order_size,
839 order.working_order_data.order_level
840 );
841 }
842 }
843 Err(e) => {
844 panic!(
845 "Failed to deserialize WorkingOrdersResponse: {}\n\nJSON was:\n{}",
846 e,
847 serde_json::to_string_pretty(&json_value).unwrap()
848 );
849 }
850 }
851 }
852}