use crate::prelude::{Account, Activity, MarketDetails};
use crate::presentation::account::{
AccountTransaction, ActivityMetadata, Position, TransactionMetadata, WorkingOrder,
};
use crate::presentation::instrument::InstrumentType;
use crate::presentation::market::{
Category, CategoryInstrument, CategoryInstrumentsMetadata, HistoricalPrice, MarketData,
MarketNavigationNode, MarketNode, PriceAllowance,
};
use crate::presentation::order::{Direction, Status};
use crate::utils::parsing::{deserialize_null_as_empty_vec, deserialize_nullable_status};
use chrono::{DateTime, Utc};
use pretty_simple_display::{DebugPretty, DisplaySimple};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(
DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default,
)]
pub struct DBEntryResponse {
pub symbol: String,
pub epic: String,
pub name: String,
pub instrument_type: InstrumentType,
pub exchange: String,
pub expiry: String,
pub last_update: DateTime<Utc>,
}
impl From<MarketNode> for DBEntryResponse {
fn from(value: MarketNode) -> Self {
let mut entry = DBEntryResponse::default();
if !value.markets.is_empty() {
let market = &value.markets[0];
entry.symbol = market
.epic
.split('.')
.nth(2)
.unwrap_or_default()
.to_string();
entry.epic = market.epic.clone();
entry.name = market.instrument_name.clone();
entry.instrument_type = market.instrument_type;
entry.exchange = "IG".to_string();
entry.expiry = market.expiry.clone();
entry.last_update = Utc::now();
}
entry
}
}
impl From<MarketData> for DBEntryResponse {
fn from(market: MarketData) -> Self {
DBEntryResponse {
symbol: market
.epic
.split('.')
.nth(2)
.unwrap_or_default()
.to_string(),
epic: market.epic.clone(),
name: market.instrument_name.clone(),
instrument_type: market.instrument_type,
exchange: "IG".to_string(),
expiry: market.expiry.clone(),
last_update: Utc::now(),
}
}
}
impl From<&MarketNode> for DBEntryResponse {
fn from(value: &MarketNode) -> Self {
DBEntryResponse::from(value.clone())
}
}
impl From<&MarketData> for DBEntryResponse {
fn from(market: &MarketData) -> Self {
DBEntryResponse::from(market.clone())
}
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct MultipleMarketDetailsResponse {
#[serde(rename = "marketDetails")]
pub market_details: Vec<MarketDetails>,
}
impl MultipleMarketDetailsResponse {
#[must_use]
pub fn len(&self) -> usize {
self.market_details.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.market_details.is_empty()
}
#[must_use]
pub fn market_details(&self) -> &Vec<MarketDetails> {
&self.market_details
}
pub fn iter(&self) -> impl Iterator<Item = &MarketDetails> {
self.market_details.iter()
}
}
#[derive(DebugPretty, Clone, Serialize, Deserialize)]
pub struct HistoricalPricesResponse {
pub prices: Vec<HistoricalPrice>,
#[serde(rename = "instrumentType")]
pub instrument_type: InstrumentType,
#[serde(rename = "allowance", skip_serializing_if = "Option::is_none", default)]
pub allowance: Option<PriceAllowance>,
}
impl HistoricalPricesResponse {
#[must_use]
pub fn len(&self) -> usize {
self.prices.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.prices.is_empty()
}
#[must_use]
pub fn prices(&self) -> &Vec<HistoricalPrice> {
&self.prices
}
pub fn iter(&self) -> impl Iterator<Item = &HistoricalPrice> {
self.prices.iter()
}
}
#[derive(DebugPretty, Clone, Serialize, Deserialize)]
pub struct MarketSearchResponse {
pub markets: Vec<MarketData>,
}
impl MarketSearchResponse {
#[must_use]
pub fn len(&self) -> usize {
self.markets.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.markets.is_empty()
}
#[must_use]
pub fn markets(&self) -> &Vec<MarketData> {
&self.markets
}
pub fn iter(&self) -> impl Iterator<Item = &MarketData> {
self.markets.iter()
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
pub struct MarketNavigationResponse {
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
pub nodes: Vec<MarketNavigationNode>,
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
pub markets: Vec<MarketData>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
pub struct CategoriesResponse {
pub categories: Vec<Category>,
}
impl CategoriesResponse {
#[must_use]
pub fn len(&self) -> usize {
self.categories.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.categories.is_empty()
}
#[must_use]
pub fn categories(&self) -> &Vec<Category> {
&self.categories
}
pub fn iter(&self) -> impl Iterator<Item = &Category> {
self.categories.iter()
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
pub struct CategoryInstrumentsResponse {
pub instruments: Vec<CategoryInstrument>,
pub metadata: Option<CategoryInstrumentsMetadata>,
}
impl CategoryInstrumentsResponse {
#[must_use]
pub fn len(&self) -> usize {
self.instruments.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.instruments.is_empty()
}
#[must_use]
pub fn instruments(&self) -> &Vec<CategoryInstrument> {
&self.instruments
}
pub fn iter(&self) -> impl Iterator<Item = &CategoryInstrument> {
self.instruments.iter()
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
pub struct AccountsResponse {
pub accounts: Vec<Account>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
pub struct PositionsResponse {
pub positions: Vec<Position>,
}
impl PositionsResponse {
pub fn compact_by_epic(positions: Vec<Position>) -> Vec<Position> {
let mut epic_map: HashMap<String, Position> = std::collections::HashMap::new();
for position in positions {
let epic = position.market.epic.clone();
epic_map
.entry(epic)
.and_modify(|existing| {
*existing = existing.clone() + position.clone();
})
.or_insert(position);
}
epic_map.into_values().collect()
}
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
pub struct WorkingOrdersResponse {
#[serde(rename = "workingOrders")]
pub working_orders: Vec<WorkingOrder>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
pub struct AccountActivityResponse {
pub activities: Vec<Activity>,
pub metadata: Option<ActivityMetadata>,
}
#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
pub struct TransactionHistoryResponse {
pub transactions: Vec<AccountTransaction>,
pub metadata: TransactionMetadata,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct CreateOrderResponse {
#[serde(rename = "dealReference")]
pub deal_reference: String,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct ClosePositionResponse {
#[serde(rename = "dealReference")]
pub deal_reference: String,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct UpdatePositionResponse {
#[serde(rename = "dealReference")]
pub deal_reference: String,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct CreateWorkingOrderResponse {
#[serde(rename = "dealReference")]
pub deal_reference: String,
}
#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
pub struct OrderConfirmationResponse {
pub date: String,
#[serde(deserialize_with = "deserialize_nullable_status")]
pub status: Status,
pub reason: Option<String>,
#[serde(rename = "dealId")]
pub deal_id: Option<String>,
#[serde(rename = "dealReference")]
pub deal_reference: String,
#[serde(rename = "dealStatus")]
pub deal_status: Option<String>,
pub epic: Option<String>,
#[serde(rename = "expiry")]
pub expiry: Option<String>,
#[serde(rename = "guaranteedStop")]
pub guaranteed_stop: Option<bool>,
#[serde(rename = "level")]
pub level: Option<f64>,
#[serde(rename = "limitDistance")]
pub limit_distance: Option<f64>,
#[serde(rename = "limitLevel")]
pub limit_level: Option<f64>,
pub size: Option<f64>,
#[serde(rename = "stopDistance")]
pub stop_distance: Option<f64>,
#[serde(rename = "stopLevel")]
pub stop_level: Option<f64>,
#[serde(rename = "trailingStop")]
pub trailing_stop: Option<bool>,
pub direction: Option<Direction>,
}
impl std::fmt::Display for MultipleMarketDetailsResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use prettytable::format;
use prettytable::{Cell, Row, Table};
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_BOX_CHARS);
table.add_row(Row::new(vec![
Cell::new("INSTRUMENT NAME"),
Cell::new("EPIC"),
Cell::new("BID"),
Cell::new("OFFER"),
Cell::new("MID"),
Cell::new("SPREAD"),
Cell::new("EXPIRY"),
Cell::new("HIGH/LOW"),
]));
let mut sorted_details = self.market_details.clone();
sorted_details.sort_by(|a, b| {
a.instrument
.name
.to_lowercase()
.cmp(&b.instrument.name.to_lowercase())
});
for details in &sorted_details {
let bid = details
.snapshot
.bid
.map(|b| format!("{:.2}", b))
.unwrap_or_else(|| "-".to_string());
let offer = details
.snapshot
.offer
.map(|o| format!("{:.2}", o))
.unwrap_or_else(|| "-".to_string());
let mid = match (details.snapshot.bid, details.snapshot.offer) {
(Some(b), Some(o)) => format!("{:.2}", (b + o) / 2.0),
_ => "-".to_string(),
};
let spread = match (details.snapshot.bid, details.snapshot.offer) {
(Some(b), Some(o)) => format!("{:.2}", o - b),
_ => "-".to_string(),
};
let expiry = details
.instrument
.expiry_details
.as_ref()
.map(|ed| {
ed.last_dealing_date
.split('T')
.next()
.unwrap_or(&ed.last_dealing_date)
.to_string()
})
.unwrap_or_else(|| {
details
.instrument
.expiry
.split('T')
.next()
.unwrap_or(&details.instrument.expiry)
.to_string()
});
let high_low = format!(
"{}/{}",
details
.snapshot
.high
.map(|h| format!("{:.2}", h))
.unwrap_or_else(|| "-".to_string()),
details
.snapshot
.low
.map(|l| format!("{:.2}", l))
.unwrap_or_else(|| "-".to_string())
);
let name = if details.instrument.name.len() > 30 {
format!("{}...", &details.instrument.name[0..27])
} else {
details.instrument.name.clone()
};
let epic = details.instrument.epic.clone();
table.add_row(Row::new(vec![
Cell::new(&name),
Cell::new(&epic),
Cell::new(&bid),
Cell::new(&offer),
Cell::new(&mid),
Cell::new(&spread),
Cell::new(&expiry),
Cell::new(&high_low),
]));
}
write!(f, "{}", table)
}
}
impl std::fmt::Display for HistoricalPricesResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use prettytable::format;
use prettytable::{Cell, Row, Table};
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_BOX_CHARS);
table.add_row(Row::new(vec![
Cell::new("SNAPSHOT TIME"),
Cell::new("OPEN BID"),
Cell::new("OPEN ASK"),
Cell::new("HIGH BID"),
Cell::new("HIGH ASK"),
Cell::new("LOW BID"),
Cell::new("LOW ASK"),
Cell::new("CLOSE BID"),
Cell::new("CLOSE ASK"),
Cell::new("VOLUME"),
]));
for price in &self.prices {
let open_bid = price
.open_price
.bid
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "-".to_string());
let open_ask = price
.open_price
.ask
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "-".to_string());
let high_bid = price
.high_price
.bid
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "-".to_string());
let high_ask = price
.high_price
.ask
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "-".to_string());
let low_bid = price
.low_price
.bid
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "-".to_string());
let low_ask = price
.low_price
.ask
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "-".to_string());
let close_bid = price
.close_price
.bid
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "-".to_string());
let close_ask = price
.close_price
.ask
.map(|v| format!("{:.4}", v))
.unwrap_or_else(|| "-".to_string());
let volume = price
.last_traded_volume
.map(|v| v.to_string())
.unwrap_or_else(|| "-".to_string());
table.add_row(Row::new(vec![
Cell::new(&price.snapshot_time),
Cell::new(&open_bid),
Cell::new(&open_ask),
Cell::new(&high_bid),
Cell::new(&high_ask),
Cell::new(&low_bid),
Cell::new(&low_ask),
Cell::new(&close_bid),
Cell::new(&close_ask),
Cell::new(&volume),
]));
}
writeln!(f, "{}", table)?;
writeln!(f, "\nSummary:")?;
writeln!(f, " Total price points: {}", self.prices.len())?;
writeln!(f, " Instrument type: {:?}", self.instrument_type)?;
if let Some(allowance) = &self.allowance {
writeln!(
f,
" Remaining allowance: {}",
allowance.remaining_allowance
)?;
writeln!(f, " Total allowance: {}", allowance.total_allowance)?;
}
Ok(())
}
}
impl std::fmt::Display for MarketSearchResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use prettytable::format;
use prettytable::{Cell, Row, Table};
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_BOX_CHARS);
table.add_row(Row::new(vec![
Cell::new("INSTRUMENT NAME"),
Cell::new("EPIC"),
Cell::new("BID"),
Cell::new("OFFER"),
Cell::new("MID"),
Cell::new("SPREAD"),
Cell::new("EXPIRY"),
Cell::new("TYPE"),
]));
let mut sorted_markets = self.markets.clone();
sorted_markets.sort_by(|a, b| {
a.instrument_name
.to_lowercase()
.cmp(&b.instrument_name.to_lowercase())
});
for market in &sorted_markets {
let bid = market
.bid
.map(|b| format!("{:.4}", b))
.unwrap_or_else(|| "-".to_string());
let offer = market
.offer
.map(|o| format!("{:.4}", o))
.unwrap_or_else(|| "-".to_string());
let mid = match (market.bid, market.offer) {
(Some(b), Some(o)) => format!("{:.4}", (b + o) / 2.0),
_ => "-".to_string(),
};
let spread = match (market.bid, market.offer) {
(Some(b), Some(o)) => format!("{:.4}", o - b),
_ => "-".to_string(),
};
let name = if market.instrument_name.len() > 30 {
format!("{}...", &market.instrument_name[0..27])
} else {
market.instrument_name.clone()
};
let expiry = market
.expiry
.split('T')
.next()
.unwrap_or(&market.expiry)
.to_string();
let instrument_type = format!("{:?}", market.instrument_type);
table.add_row(Row::new(vec![
Cell::new(&name),
Cell::new(&market.epic),
Cell::new(&bid),
Cell::new(&offer),
Cell::new(&mid),
Cell::new(&spread),
Cell::new(&expiry),
Cell::new(&instrument_type),
]));
}
writeln!(f, "{}", table)?;
writeln!(f, "\nTotal markets found: {}", self.markets.len())?;
Ok(())
}
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct WatchlistsResponse {
pub watchlists: Vec<Watchlist>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct Watchlist {
pub id: String,
pub name: String,
pub editable: bool,
pub deleteable: bool,
#[serde(rename = "defaultSystemWatchlist")]
pub default_system_watchlist: bool,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct CreateWatchlistResponse {
#[serde(rename = "watchlistId")]
pub watchlist_id: String,
pub status: String,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct WatchlistMarketsResponse {
pub markets: Vec<MarketData>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct StatusResponse {
pub status: String,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct ClientSentimentResponse {
#[serde(rename = "clientSentiments")]
pub client_sentiments: Vec<MarketSentiment>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct MarketSentiment {
#[serde(rename = "marketId")]
pub market_id: String,
#[serde(rename = "longPositionPercentage")]
pub long_position_percentage: f64,
#[serde(rename = "shortPositionPercentage")]
pub short_position_percentage: f64,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct IndicativeCostsResponse {
#[serde(rename = "indicativeQuoteReference")]
pub indicative_quote_reference: String,
#[serde(rename = "costsAndCharges")]
pub costs_and_charges: CostsAndCharges,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct CostsAndCharges {
#[serde(rename = "totalCostPercentage")]
pub total_cost_percentage: Option<f64>,
#[serde(rename = "totalCostAmount")]
pub total_cost_amount: Option<f64>,
pub currency: Option<String>,
#[serde(rename = "oneOffCosts")]
pub one_off_costs: Option<CostBreakdown>,
#[serde(rename = "ongoingCosts")]
pub ongoing_costs: Option<CostBreakdown>,
#[serde(rename = "transactionCosts")]
pub transaction_costs: Option<CostBreakdown>,
#[serde(rename = "incidentalCosts")]
pub incidental_costs: Option<CostBreakdown>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct CostBreakdown {
pub percentage: Option<f64>,
pub amount: Option<f64>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct CostsHistoryResponse {
pub costs: Vec<HistoricalCost>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct HistoricalCost {
pub date: String,
#[serde(rename = "dealReference")]
pub deal_reference: Option<String>,
pub epic: Option<String>,
#[serde(rename = "totalCost")]
pub total_cost: Option<f64>,
pub currency: Option<String>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct DurableMediumResponse {
pub document: String,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct AccountPreferencesResponse {
#[serde(rename = "trailingStopsEnabled")]
pub trailing_stops_enabled: bool,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct ApplicationDetailsResponse {
#[serde(rename = "apiKey")]
pub api_key: String,
pub name: Option<String>,
pub status: String,
#[serde(rename = "allowanceAccountOverall")]
pub allowance_account_overall: Option<i32>,
#[serde(rename = "allowanceAccountTrading")]
pub allowance_account_trading: Option<i32>,
#[serde(rename = "concurrentSubscriptionsLimit")]
pub concurrent_subscriptions_limit: Option<i32>,
#[serde(rename = "createdDate")]
pub created_date: Option<String>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize, Default)]
pub struct ApplicationInfo {
#[serde(rename = "apiKey")]
pub api_key: String,
pub name: Option<String>,
pub status: String,
#[serde(rename = "allowanceAccountOverall")]
pub allowance_account_overall: Option<i32>,
#[serde(rename = "allowanceAccountTrading")]
pub allowance_account_trading: Option<i32>,
#[serde(rename = "concurrentSubscriptionsLimit")]
pub concurrent_subscriptions_limit: Option<i32>,
#[serde(rename = "createdDate")]
pub created_date: Option<String>,
}
#[derive(DebugPretty, Clone, Serialize, Deserialize)]
pub struct SinglePositionResponse {
pub position: Position,
pub market: MarketData,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use std::fs;
#[test]
#[ignore = "requires Data/working_orders.json file"]
fn test_deserialize_working_orders_from_file() -> Result<(), Box<dyn std::error::Error>> {
let json_content = fs::read_to_string("Data/working_orders.json")?;
let json_value: Value = serde_json::from_str(&json_content)?;
println!(
"JSON structure:\n{}",
serde_json::to_string_pretty(&json_value)?
);
let response: WorkingOrdersResponse = serde_json::from_str(&json_content)?;
println!(
"Successfully deserialized {} working orders",
response.working_orders.len()
);
for (idx, order) in response.working_orders.iter().enumerate() {
println!(
"Order {}: epic={}, direction={:?}, size={}, level={}",
idx + 1,
order.working_order_data.epic,
order.working_order_data.direction,
order.working_order_data.order_size,
order.working_order_data.order_level
);
}
Ok(())
}
}