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 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#[derive(
23 DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default,
24)]
25pub struct DBEntryResponse {
26 pub symbol: String,
28 pub epic: String,
30 pub name: String,
32 pub instrument_type: InstrumentType,
34 pub exchange: String,
36 pub expiry: String,
38 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#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
97pub struct MultipleMarketDetailsResponse {
98 #[serde(rename = "marketDetails")]
100 pub market_details: Vec<MarketDetails>,
101}
102
103impl MultipleMarketDetailsResponse {
104 #[must_use]
109 pub fn len(&self) -> usize {
110 self.market_details.len()
111 }
112
113 #[must_use]
118 pub fn is_empty(&self) -> bool {
119 self.market_details.is_empty()
120 }
121
122 #[must_use]
127 pub fn market_details(&self) -> &Vec<MarketDetails> {
128 &self.market_details
129 }
130
131 pub fn iter(&self) -> impl Iterator<Item = &MarketDetails> {
136 self.market_details.iter()
137 }
138}
139
140#[derive(DebugPretty, Clone, Serialize, Deserialize)]
142pub struct HistoricalPricesResponse {
143 pub prices: Vec<HistoricalPrice>,
145 #[serde(rename = "instrumentType")]
147 pub instrument_type: InstrumentType,
148 #[serde(rename = "allowance", skip_serializing_if = "Option::is_none", default)]
150 pub allowance: Option<PriceAllowance>,
151}
152
153impl HistoricalPricesResponse {
154 #[must_use]
159 pub fn len(&self) -> usize {
160 self.prices.len()
161 }
162
163 #[must_use]
168 pub fn is_empty(&self) -> bool {
169 self.prices.is_empty()
170 }
171
172 #[must_use]
177 pub fn prices(&self) -> &Vec<HistoricalPrice> {
178 &self.prices
179 }
180
181 pub fn iter(&self) -> impl Iterator<Item = &HistoricalPrice> {
186 self.prices.iter()
187 }
188}
189
190#[derive(DebugPretty, Clone, Serialize, Deserialize)]
192pub struct MarketSearchResponse {
193 pub markets: Vec<MarketData>,
195}
196
197impl MarketSearchResponse {
198 #[must_use]
203 pub fn len(&self) -> usize {
204 self.markets.len()
205 }
206
207 #[must_use]
212 pub fn is_empty(&self) -> bool {
213 self.markets.is_empty()
214 }
215
216 #[must_use]
221 pub fn markets(&self) -> &Vec<MarketData> {
222 &self.markets
223 }
224
225 pub fn iter(&self) -> impl Iterator<Item = &MarketData> {
230 self.markets.iter()
231 }
232}
233
234#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
236pub struct MarketNavigationResponse {
237 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
239 pub nodes: Vec<MarketNavigationNode>,
240 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
242 pub markets: Vec<MarketData>,
243}
244
245#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
247pub struct AccountsResponse {
248 pub accounts: Vec<Account>,
250}
251
252#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
254pub struct PositionsResponse {
255 pub positions: Vec<Position>,
257}
258
259impl PositionsResponse {
260 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#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
289pub struct WorkingOrdersResponse {
290 #[serde(rename = "workingOrders")]
292 pub working_orders: Vec<WorkingOrder>,
293}
294
295#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
297pub struct AccountActivityResponse {
298 pub activities: Vec<Activity>,
300 pub metadata: Option<ActivityMetadata>,
302}
303
304#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
306pub struct TransactionHistoryResponse {
307 pub transactions: Vec<AccountTransaction>,
309 pub metadata: TransactionMetadata,
311}
312
313#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
315pub struct CreateOrderResponse {
316 #[serde(rename = "dealReference")]
318 pub deal_reference: String,
319}
320
321#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
323pub struct ClosePositionResponse {
324 #[serde(rename = "dealReference")]
326 pub deal_reference: String,
327}
328
329#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
331pub struct UpdatePositionResponse {
332 #[serde(rename = "dealReference")]
334 pub deal_reference: String,
335}
336
337#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
339pub struct CreateWorkingOrderResponse {
340 #[serde(rename = "dealReference")]
342 pub deal_reference: String,
343}
344
345#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
347pub struct OrderConfirmationResponse {
348 pub date: String,
350 #[serde(deserialize_with = "deserialize_nullable_status")]
353 pub status: Status,
354 pub reason: Option<String>,
356 #[serde(rename = "dealId")]
358 pub deal_id: Option<String>,
359 #[serde(rename = "dealReference")]
361 pub deal_reference: String,
362 #[serde(rename = "dealStatus")]
364 pub deal_status: Option<String>,
365 pub epic: Option<String>,
367 #[serde(rename = "expiry")]
369 pub expiry: Option<String>,
370 #[serde(rename = "guaranteedStop")]
372 pub guaranteed_stop: Option<bool>,
373 #[serde(rename = "level")]
375 pub level: Option<f64>,
376 #[serde(rename = "limitDistance")]
378 pub limit_distance: Option<f64>,
379 #[serde(rename = "limitLevel")]
381 pub limit_level: Option<f64>,
382 pub size: Option<f64>,
384 #[serde(rename = "stopDistance")]
386 pub stop_distance: Option<f64>,
387 #[serde(rename = "stopLevel")]
389 pub stop_level: Option<f64>,
390 #[serde(rename = "trailingStop")]
392 pub trailing_stop: Option<bool>,
393 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 table.set_format(*format::consts::FORMAT_BOX_CHARS);
406
407 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 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 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 let expiry = details
454 .instrument
455 .expiry_details
456 .as_ref()
457 .map(|ed| {
458 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 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 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 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 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 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 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 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 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 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 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 let json_content = fs::read_to_string("Data/working_orders.json")
721 .expect("Failed to read Data/working_orders.json");
722
723 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 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}