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
140impl std::fmt::Display for MultipleMarketDetailsResponse {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 use prettytable::format;
143 use prettytable::{Cell, Row, Table};
144
145 let mut table = Table::new();
146
147 table.set_format(*format::consts::FORMAT_BOX_CHARS);
149
150 table.add_row(Row::new(vec![
152 Cell::new("INSTRUMENT NAME"),
153 Cell::new("EPIC"),
154 Cell::new("BID"),
155 Cell::new("OFFER"),
156 Cell::new("MID"),
157 Cell::new("SPREAD"),
158 Cell::new("EXPIRY"),
159 Cell::new("HIGH/LOW"),
160 ]));
161
162 let mut sorted_details = self.market_details.clone();
164 sorted_details.sort_by(|a, b| {
165 a.instrument
166 .name
167 .to_lowercase()
168 .cmp(&b.instrument.name.to_lowercase())
169 });
170
171 for details in &sorted_details {
173 let bid = details
174 .snapshot
175 .bid
176 .map(|b| format!("{:.2}", b))
177 .unwrap_or_else(|| "-".to_string());
178
179 let offer = details
180 .snapshot
181 .offer
182 .map(|o| format!("{:.2}", o))
183 .unwrap_or_else(|| "-".to_string());
184
185 let mid = match (details.snapshot.bid, details.snapshot.offer) {
186 (Some(b), Some(o)) => format!("{:.2}", (b + o) / 2.0),
187 _ => "-".to_string(),
188 };
189
190 let spread = match (details.snapshot.bid, details.snapshot.offer) {
191 (Some(b), Some(o)) => format!("{:.2}", o - b),
192 _ => "-".to_string(),
193 };
194
195 let expiry = details
197 .instrument
198 .expiry_details
199 .as_ref()
200 .map(|ed| {
201 ed.last_dealing_date
203 .split('T')
204 .next()
205 .unwrap_or(&ed.last_dealing_date)
206 .to_string()
207 })
208 .unwrap_or_else(|| {
209 details
210 .instrument
211 .expiry
212 .split('T')
213 .next()
214 .unwrap_or(&details.instrument.expiry)
215 .to_string()
216 });
217
218 let high_low = format!(
219 "{}/{}",
220 details
221 .snapshot
222 .high
223 .map(|h| format!("{:.2}", h))
224 .unwrap_or_else(|| "-".to_string()),
225 details
226 .snapshot
227 .low
228 .map(|l| format!("{:.2}", l))
229 .unwrap_or_else(|| "-".to_string())
230 );
231
232 let name = if details.instrument.name.len() > 30 {
234 format!("{}...", &details.instrument.name[0..27])
235 } else {
236 details.instrument.name.clone()
237 };
238
239 let epic = details.instrument.epic.clone();
241
242 table.add_row(Row::new(vec![
243 Cell::new(&name),
244 Cell::new(&epic),
245 Cell::new(&bid),
246 Cell::new(&offer),
247 Cell::new(&mid),
248 Cell::new(&spread),
249 Cell::new(&expiry),
250 Cell::new(&high_low),
251 ]));
252 }
253
254 write!(f, "{}", table)
255 }
256}
257
258#[derive(DebugPretty, Clone, Serialize, Deserialize)]
260pub struct HistoricalPricesResponse {
261 pub prices: Vec<HistoricalPrice>,
263 #[serde(rename = "instrumentType")]
265 pub instrument_type: InstrumentType,
266 #[serde(rename = "allowance", skip_serializing_if = "Option::is_none", default)]
268 pub allowance: Option<PriceAllowance>,
269}
270
271impl HistoricalPricesResponse {
272 #[must_use]
277 pub fn len(&self) -> usize {
278 self.prices.len()
279 }
280
281 #[must_use]
286 pub fn is_empty(&self) -> bool {
287 self.prices.is_empty()
288 }
289
290 #[must_use]
295 pub fn prices(&self) -> &Vec<HistoricalPrice> {
296 &self.prices
297 }
298
299 pub fn iter(&self) -> impl Iterator<Item = &HistoricalPrice> {
304 self.prices.iter()
305 }
306}
307
308impl std::fmt::Display for HistoricalPricesResponse {
309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 use prettytable::format;
311 use prettytable::{Cell, Row, Table};
312
313 let mut table = Table::new();
314 table.set_format(*format::consts::FORMAT_BOX_CHARS);
315
316 table.add_row(Row::new(vec![
318 Cell::new("SNAPSHOT TIME"),
319 Cell::new("OPEN BID"),
320 Cell::new("OPEN ASK"),
321 Cell::new("HIGH BID"),
322 Cell::new("HIGH ASK"),
323 Cell::new("LOW BID"),
324 Cell::new("LOW ASK"),
325 Cell::new("CLOSE BID"),
326 Cell::new("CLOSE ASK"),
327 Cell::new("VOLUME"),
328 ]));
329
330 for price in &self.prices {
332 let open_bid = price
333 .open_price
334 .bid
335 .map(|v| format!("{:.4}", v))
336 .unwrap_or_else(|| "-".to_string());
337
338 let open_ask = price
339 .open_price
340 .ask
341 .map(|v| format!("{:.4}", v))
342 .unwrap_or_else(|| "-".to_string());
343
344 let high_bid = price
345 .high_price
346 .bid
347 .map(|v| format!("{:.4}", v))
348 .unwrap_or_else(|| "-".to_string());
349
350 let high_ask = price
351 .high_price
352 .ask
353 .map(|v| format!("{:.4}", v))
354 .unwrap_or_else(|| "-".to_string());
355
356 let low_bid = price
357 .low_price
358 .bid
359 .map(|v| format!("{:.4}", v))
360 .unwrap_or_else(|| "-".to_string());
361
362 let low_ask = price
363 .low_price
364 .ask
365 .map(|v| format!("{:.4}", v))
366 .unwrap_or_else(|| "-".to_string());
367
368 let close_bid = price
369 .close_price
370 .bid
371 .map(|v| format!("{:.4}", v))
372 .unwrap_or_else(|| "-".to_string());
373
374 let close_ask = price
375 .close_price
376 .ask
377 .map(|v| format!("{:.4}", v))
378 .unwrap_or_else(|| "-".to_string());
379
380 let volume = price
381 .last_traded_volume
382 .map(|v| v.to_string())
383 .unwrap_or_else(|| "-".to_string());
384
385 table.add_row(Row::new(vec![
386 Cell::new(&price.snapshot_time),
387 Cell::new(&open_bid),
388 Cell::new(&open_ask),
389 Cell::new(&high_bid),
390 Cell::new(&high_ask),
391 Cell::new(&low_bid),
392 Cell::new(&low_ask),
393 Cell::new(&close_bid),
394 Cell::new(&close_ask),
395 Cell::new(&volume),
396 ]));
397 }
398
399 writeln!(f, "{}", table)?;
401 writeln!(f, "\nSummary:")?;
402 writeln!(f, " Total price points: {}", self.prices.len())?;
403 writeln!(f, " Instrument type: {:?}", self.instrument_type)?;
404
405 if let Some(allowance) = &self.allowance {
406 writeln!(
407 f,
408 " Remaining allowance: {}",
409 allowance.remaining_allowance
410 )?;
411 writeln!(f, " Total allowance: {}", allowance.total_allowance)?;
412 }
413
414 Ok(())
415 }
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct MarketSearchResponse {
421 pub markets: Vec<MarketData>,
423}
424
425impl MarketSearchResponse {
426 #[must_use]
431 pub fn len(&self) -> usize {
432 self.markets.len()
433 }
434
435 #[must_use]
440 pub fn is_empty(&self) -> bool {
441 self.markets.is_empty()
442 }
443
444 #[must_use]
449 pub fn markets(&self) -> &Vec<MarketData> {
450 &self.markets
451 }
452
453 pub fn iter(&self) -> impl Iterator<Item = &MarketData> {
458 self.markets.iter()
459 }
460}
461
462impl std::fmt::Display for MarketSearchResponse {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 use prettytable::format;
465 use prettytable::{Cell, Row, Table};
466
467 let mut table = Table::new();
468 table.set_format(*format::consts::FORMAT_BOX_CHARS);
469
470 table.add_row(Row::new(vec![
472 Cell::new("INSTRUMENT NAME"),
473 Cell::new("EPIC"),
474 Cell::new("BID"),
475 Cell::new("OFFER"),
476 Cell::new("MID"),
477 Cell::new("SPREAD"),
478 Cell::new("EXPIRY"),
479 Cell::new("TYPE"),
480 ]));
481
482 let mut sorted_markets = self.markets.clone();
484 sorted_markets.sort_by(|a, b| {
485 a.instrument_name
486 .to_lowercase()
487 .cmp(&b.instrument_name.to_lowercase())
488 });
489
490 for market in &sorted_markets {
492 let bid = market
493 .bid
494 .map(|b| format!("{:.4}", b))
495 .unwrap_or_else(|| "-".to_string());
496
497 let offer = market
498 .offer
499 .map(|o| format!("{:.4}", o))
500 .unwrap_or_else(|| "-".to_string());
501
502 let mid = match (market.bid, market.offer) {
503 (Some(b), Some(o)) => format!("{:.4}", (b + o) / 2.0),
504 _ => "-".to_string(),
505 };
506
507 let spread = match (market.bid, market.offer) {
508 (Some(b), Some(o)) => format!("{:.4}", o - b),
509 _ => "-".to_string(),
510 };
511
512 let name = if market.instrument_name.len() > 30 {
514 format!("{}...", &market.instrument_name[0..27])
515 } else {
516 market.instrument_name.clone()
517 };
518
519 let expiry = market
521 .expiry
522 .split('T')
523 .next()
524 .unwrap_or(&market.expiry)
525 .to_string();
526
527 let instrument_type = format!("{:?}", market.instrument_type);
528
529 table.add_row(Row::new(vec![
530 Cell::new(&name),
531 Cell::new(&market.epic),
532 Cell::new(&bid),
533 Cell::new(&offer),
534 Cell::new(&mid),
535 Cell::new(&spread),
536 Cell::new(&expiry),
537 Cell::new(&instrument_type),
538 ]));
539 }
540
541 writeln!(f, "{}", table)?;
542 writeln!(f, "\nTotal markets found: {}", self.markets.len())?;
543
544 Ok(())
545 }
546}
547
548#[derive(Debug, Clone, Deserialize, Serialize)]
550pub struct MarketNavigationResponse {
551 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
553 pub nodes: Vec<MarketNavigationNode>,
554 #[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
556 pub markets: Vec<MarketData>,
557}
558
559#[derive(Debug, Clone, Deserialize)]
561pub struct AccountsResponse {
562 pub accounts: Vec<Account>,
564}
565
566#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize, Default)]
568pub struct PositionsResponse {
569 pub positions: Vec<Position>,
571}
572
573impl PositionsResponse {
574 pub fn compact_by_epic(positions: Vec<Position>) -> Vec<Position> {
585 let mut epic_map: HashMap<String, Position> = std::collections::HashMap::new();
586
587 for position in positions {
588 let epic = position.market.epic.clone();
589 epic_map
590 .entry(epic)
591 .and_modify(|existing| {
592 *existing = existing.clone() + position.clone();
593 })
594 .or_insert(position);
595 }
596
597 epic_map.into_values().collect()
598 }
599}
600
601#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
603pub struct WorkingOrdersResponse {
604 #[serde(rename = "workingOrders")]
606 pub working_orders: Vec<WorkingOrder>,
607}
608
609#[derive(Debug, Clone, Deserialize)]
611pub struct AccountActivityResponse {
612 pub activities: Vec<Activity>,
614 pub metadata: Option<ActivityMetadata>,
616}
617
618#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
620pub struct TransactionHistoryResponse {
621 pub transactions: Vec<AccountTransaction>,
623 pub metadata: TransactionMetadata,
625}
626
627#[derive(Debug, Clone, DisplaySimple, Serialize, Deserialize)]
629pub struct CreateOrderResponse {
630 #[serde(rename = "dealReference")]
632 pub deal_reference: String,
633}
634
635#[derive(Debug, Clone, DisplaySimple, Serialize, Deserialize)]
637pub struct ClosePositionResponse {
638 #[serde(rename = "dealReference")]
640 pub deal_reference: String,
641}
642
643#[derive(Debug, Clone, DisplaySimple, Serialize, Deserialize)]
645pub struct UpdatePositionResponse {
646 #[serde(rename = "dealReference")]
648 pub deal_reference: String,
649}
650
651#[derive(Debug, Clone, DisplaySimple, Serialize, Deserialize)]
653pub struct CreateWorkingOrderResponse {
654 #[serde(rename = "dealReference")]
656 pub deal_reference: String,
657}
658
659#[derive(Debug, Clone, DisplaySimple, Serialize, Deserialize)]
661pub struct OrderConfirmationResponse {
662 pub date: String,
664 #[serde(deserialize_with = "deserialize_nullable_status")]
667 pub status: Status,
668 pub reason: Option<String>,
670 #[serde(rename = "dealId")]
672 pub deal_id: Option<String>,
673 #[serde(rename = "dealReference")]
675 pub deal_reference: String,
676 #[serde(rename = "dealStatus")]
678 pub deal_status: Option<String>,
679 pub epic: Option<String>,
681 #[serde(rename = "expiry")]
683 pub expiry: Option<String>,
684 #[serde(rename = "guaranteedStop")]
686 pub guaranteed_stop: Option<bool>,
687 #[serde(rename = "level")]
689 pub level: Option<f64>,
690 #[serde(rename = "limitDistance")]
692 pub limit_distance: Option<f64>,
693 #[serde(rename = "limitLevel")]
695 pub limit_level: Option<f64>,
696 pub size: Option<f64>,
698 #[serde(rename = "stopDistance")]
700 pub stop_distance: Option<f64>,
701 #[serde(rename = "stopLevel")]
703 pub stop_level: Option<f64>,
704 #[serde(rename = "trailingStop")]
706 pub trailing_stop: Option<bool>,
707 pub direction: Option<Direction>,
709}