ig_client/application/models/
account.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 13/5/25
5******************************************************************************/
6use super::order::{Direction, OrderType, Status, TimeInForce};
7use crate::application::models::market::InstrumentType;
8use crate::presentation::MarketState;
9use pretty_simple_display::DisplaySimple;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::ops::Add;
13
14/// Account information
15#[derive(Debug, Clone, Deserialize)]
16pub struct AccountInfo {
17    /// List of accounts owned by the user
18    pub accounts: Vec<Account>,
19}
20
21/// Details of a specific account
22#[derive(Debug, Clone, Deserialize)]
23pub struct Account {
24    /// Unique identifier for the account
25    #[serde(rename = "accountId")]
26    pub account_id: String,
27    /// Name of the account
28    #[serde(rename = "accountName")]
29    pub account_name: String,
30    /// Type of the account (e.g., CFD, Spread bet)
31    #[serde(rename = "accountType")]
32    pub account_type: String,
33    /// Balance information for the account
34    pub balance: AccountBalance,
35    /// Base currency of the account
36    pub currency: String,
37    /// Current status of the account
38    pub status: String,
39    /// Whether this is the preferred account
40    pub preferred: bool,
41}
42
43/// Account balance information
44#[derive(Debug, Clone, Deserialize)]
45pub struct AccountBalance {
46    /// Total balance of the account
47    pub balance: f64,
48    /// Deposit amount
49    pub deposit: f64,
50    /// Current profit or loss
51    #[serde(rename = "profitLoss")]
52    pub profit_loss: f64,
53    /// Available funds for trading
54    pub available: f64,
55}
56
57/// Account activity
58#[derive(Debug, Clone, Deserialize)]
59pub struct AccountActivity {
60    /// List of activities on the account
61    pub activities: Vec<Activity>,
62    /// Metadata about pagination
63    pub metadata: Option<ActivityMetadata>,
64}
65
66/// Metadata for activity pagination
67#[derive(Debug, Clone, Deserialize)]
68pub struct ActivityMetadata {
69    /// Paging information
70    pub paging: Option<ActivityPaging>,
71}
72
73/// Paging information for activities
74#[derive(Debug, Clone, Deserialize)]
75pub struct ActivityPaging {
76    /// Number of items per page
77    pub size: Option<i32>,
78    /// URL for the next page of results
79    pub next: Option<String>,
80}
81
82#[derive(Debug, Copy, Clone, DisplaySimple, Deserialize, Serialize)]
83/// Type of account activity
84pub enum ActivityType {
85    /// Activity related to editing stop and limit orders
86    #[serde(rename = "EDIT_STOP_AND_LIMIT")]
87    EditStopAndLimit,
88    /// Activity related to positions
89    #[serde(rename = "POSITION")]
90    Position,
91    /// System-generated activity
92    #[serde(rename = "SYSTEM")]
93    System,
94    /// Activity related to working orders
95    #[serde(rename = "WORKING_ORDER")]
96    WorkingOrder,
97}
98
99/// Individual activity record
100#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
101pub struct Activity {
102    /// Date and time of the activity
103    pub date: String,
104    /// Unique identifier for the deal
105    #[serde(rename = "dealId", default)]
106    pub deal_id: Option<String>,
107    /// Instrument EPIC identifier
108    #[serde(default)]
109    pub epic: Option<String>,
110    /// Time period of the activity
111    #[serde(default)]
112    pub period: Option<String>,
113    /// Client-generated reference for the deal
114    #[serde(rename = "dealReference", default)]
115    pub deal_reference: Option<String>,
116    /// Type of activity
117    #[serde(rename = "type")]
118    pub activity_type: ActivityType,
119    /// Status of the activity
120    #[serde(default)]
121    pub status: Option<Status>,
122    /// Description of the activity
123    #[serde(default)]
124    pub description: Option<String>,
125    /// Additional details about the activity
126    /// This is a string when detailed=false, and an object when detailed=true
127    #[serde(default)]
128    pub details: Option<ActivityDetails>,
129    /// Channel the activity occurred on (e.g., "WEB" or "Mobile")
130    #[serde(default)]
131    pub channel: Option<String>,
132    /// The currency, e.g., a pound symbol
133    #[serde(default)]
134    pub currency: Option<String>,
135    /// Price level
136    #[serde(default)]
137    pub level: Option<String>,
138}
139
140/// Detailed information about an activity
141/// Only available when using the detailed=true parameter
142#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
143pub struct ActivityDetails {
144    /// Client-generated reference for the deal
145    #[serde(rename = "dealReference", default)]
146    pub deal_reference: Option<String>,
147    /// List of actions associated with this activity
148    #[serde(default)]
149    pub actions: Vec<ActivityAction>,
150    /// Name of the market
151    #[serde(rename = "marketName", default)]
152    pub market_name: Option<String>,
153    /// Date until which the order is valid
154    #[serde(rename = "goodTillDate", default)]
155    pub good_till_date: Option<String>,
156    /// Currency of the transaction
157    #[serde(default)]
158    pub currency: Option<String>,
159    /// Size/quantity of the transaction
160    #[serde(default)]
161    pub size: Option<f64>,
162    /// Direction of the transaction (BUY or SELL)
163    #[serde(default)]
164    pub direction: Option<Direction>,
165    /// Price level
166    #[serde(default)]
167    pub level: Option<f64>,
168    /// Stop level price
169    #[serde(rename = "stopLevel", default)]
170    pub stop_level: Option<f64>,
171    /// Distance for the stop
172    #[serde(rename = "stopDistance", default)]
173    pub stop_distance: Option<f64>,
174    /// Whether the stop is guaranteed
175    #[serde(rename = "guaranteedStop", default)]
176    pub guaranteed_stop: Option<bool>,
177    /// Distance for the trailing stop
178    #[serde(rename = "trailingStopDistance", default)]
179    pub trailing_stop_distance: Option<f64>,
180    /// Step size for the trailing stop
181    #[serde(rename = "trailingStep", default)]
182    pub trailing_step: Option<f64>,
183    /// Limit level price
184    #[serde(rename = "limitLevel", default)]
185    pub limit_level: Option<f64>,
186    /// Distance for the limit
187    #[serde(rename = "limitDistance", default)]
188    pub limit_distance: Option<f64>,
189}
190
191/// Types of actions that can be performed on an activity
192#[derive(Debug, Copy, Clone, DisplaySimple, Deserialize, Serialize)]
193pub enum ActionType {
194    /// A limit order was deleted
195    #[serde(rename = "LIMIT_ORDER_DELETED")]
196    LimitOrderDeleted,
197    /// A limit order was filled
198    #[serde(rename = "LIMIT_ORDER_FILLED")]
199    LimitOrderFilled,
200    /// A limit order was opened
201    #[serde(rename = "LIMIT_ORDER_OPENED")]
202    LimitOrderOpened,
203    /// A limit order was rolled
204    #[serde(rename = "LIMIT_ORDER_ROLLED")]
205    LimitOrderRolled,
206    /// A position was closed
207    #[serde(rename = "POSITION_CLOSED")]
208    PositionClosed,
209    /// A position was deleted
210    #[serde(rename = "POSITION_DELETED")]
211    PositionDeleted,
212    /// A position was opened
213    #[serde(rename = "POSITION_OPENED")]
214    PositionOpened,
215    /// A position was partially closed
216    #[serde(rename = "POSITION_PARTIALLY_CLOSED")]
217    PositionPartiallyClosed,
218    /// A position was rolled
219    #[serde(rename = "POSITION_ROLLED")]
220    PositionRolled,
221    /// A stop/limit was amended
222    #[serde(rename = "STOP_LIMIT_AMENDED")]
223    StopLimitAmended,
224    /// A stop order was amended
225    #[serde(rename = "STOP_ORDER_AMENDED")]
226    StopOrderAmended,
227    /// A stop order was deleted
228    #[serde(rename = "STOP_ORDER_DELETED")]
229    StopOrderDeleted,
230    /// A stop order was filled
231    #[serde(rename = "STOP_ORDER_FILLED")]
232    StopOrderFilled,
233    /// A stop order was opened
234    #[serde(rename = "STOP_ORDER_OPENED")]
235    StopOrderOpened,
236    /// A stop order was rolled
237    #[serde(rename = "STOP_ORDER_ROLLED")]
238    StopOrderRolled,
239    /// Unknown action type
240    #[serde(rename = "UNKNOWN")]
241    Unknown,
242    /// A working order was deleted
243    #[serde(rename = "WORKING_ORDER_DELETED")]
244    WorkingOrderDeleted,
245}
246
247/// Action associated with an activity
248#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
249pub struct ActivityAction {
250    /// Type of action
251    #[serde(rename = "actionType")]
252    pub action_type: ActionType,
253    /// Deal ID affected by this action
254    #[serde(rename = "affectedDealId", default)]
255    pub affected_deal_id: Option<String>,
256}
257
258/// Open positions
259#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize, Default)]
260pub struct Positions {
261    /// List of open positions
262    pub positions: Vec<Position>,
263}
264
265impl Positions {
266    /// Compact positions by epic, combining positions with the same epic
267    ///
268    /// This method takes a vector of positions and returns a new vector where
269    /// positions with the same epic have been combined into a single position.
270    ///
271    /// # Arguments
272    /// * `positions` - A vector of positions to compact
273    ///
274    /// # Returns
275    /// A vector of positions with unique epics
276    pub fn compact_by_epic(positions: Vec<Position>) -> Vec<Position> {
277        let mut epic_map: HashMap<String, Position> = std::collections::HashMap::new();
278
279        for position in positions {
280            let epic = position.market.epic.clone();
281            epic_map
282                .entry(epic)
283                .and_modify(|existing| {
284                    *existing = existing.clone() + position.clone();
285                })
286                .or_insert(position);
287        }
288
289        epic_map.into_values().collect()
290    }
291}
292
293/// Individual position
294#[derive(Debug, Clone, DisplaySimple, Serialize, Deserialize)]
295pub struct Position {
296    /// Details of the position
297    pub position: PositionDetails,
298    /// Market information for the position
299    pub market: PositionMarket,
300    /// Profit and loss for the position
301    pub pnl: Option<f64>,
302}
303
304impl Add for Position {
305    type Output = Position;
306
307    fn add(self, other: Position) -> Position {
308        if self.market.epic != other.market.epic {
309            panic!("Cannot add positions from different markets");
310        }
311        Position {
312            position: self.position + other.position,
313            market: self.market,
314            pnl: match (self.pnl, other.pnl) {
315                (Some(a), Some(b)) => Some(a + b),
316                (Some(a), None) => Some(a),
317                (None, Some(b)) => Some(b),
318                (None, None) => None,
319            },
320        }
321    }
322}
323
324/// Details of a position
325#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
326pub struct PositionDetails {
327    /// Size of one contract
328    #[serde(rename = "contractSize")]
329    pub contract_size: f64,
330    /// Date and time when the position was created
331    #[serde(rename = "createdDate")]
332    pub created_date: String,
333    /// UTC date and time when the position was created
334    #[serde(rename = "createdDateUTC")]
335    pub created_date_utc: String,
336    /// Unique identifier for the deal
337    #[serde(rename = "dealId")]
338    pub deal_id: String,
339    /// Client-generated reference for the deal
340    #[serde(rename = "dealReference")]
341    pub deal_reference: String,
342    /// Direction of the position (buy or sell)
343    pub direction: Direction,
344    /// Price level for take profit
345    #[serde(rename = "limitLevel")]
346    pub limit_level: Option<f64>,
347    /// Opening price level of the position
348    pub level: f64,
349    /// Size/quantity of the position
350    pub size: f64,
351    /// Price level for stop loss
352    #[serde(rename = "stopLevel")]
353    pub stop_level: Option<f64>,
354    /// Step size for trailing stop
355    #[serde(rename = "trailingStep")]
356    pub trailing_step: Option<f64>,
357    /// Distance for trailing stop
358    #[serde(rename = "trailingStopDistance")]
359    pub trailing_stop_distance: Option<f64>,
360    /// Currency of the position
361    pub currency: String,
362    /// Whether the position has controlled risk
363    #[serde(rename = "controlledRisk")]
364    pub controlled_risk: bool,
365    /// Premium paid for limited risk
366    #[serde(rename = "limitedRiskPremium")]
367    pub limited_risk_premium: Option<f64>,
368}
369
370impl Add for PositionDetails {
371    type Output = PositionDetails;
372
373    fn add(self, other: PositionDetails) -> PositionDetails {
374        let (contract_size, size) = if self.direction != other.direction {
375            (
376                (self.contract_size - other.contract_size).abs(),
377                (self.size - other.size).abs(),
378            )
379        } else {
380            (
381                self.contract_size + other.contract_size,
382                self.size + other.size,
383            )
384        };
385
386        PositionDetails {
387            contract_size,
388            created_date: self.created_date,
389            created_date_utc: self.created_date_utc,
390            deal_id: self.deal_id,
391            deal_reference: self.deal_reference,
392            direction: self.direction,
393            limit_level: other.limit_level.or(self.limit_level),
394            level: (self.level + other.level) / 2.0, // Average level
395            size,
396            stop_level: other.stop_level.or(self.stop_level),
397            trailing_step: other.trailing_step.or(self.trailing_step),
398            trailing_stop_distance: other.trailing_stop_distance.or(self.trailing_stop_distance),
399            currency: self.currency.clone(),
400            controlled_risk: self.controlled_risk || other.controlled_risk,
401            limited_risk_premium: other.limited_risk_premium.or(self.limited_risk_premium),
402        }
403    }
404}
405
406/// Market information for a position
407#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
408pub struct PositionMarket {
409    /// Human-readable name of the instrument
410    #[serde(rename = "instrumentName")]
411    pub instrument_name: String,
412    /// Expiry date of the instrument
413    pub expiry: String,
414    /// Unique identifier for the market
415    pub epic: String,
416    /// Type of the instrument
417    #[serde(rename = "instrumentType")]
418    pub instrument_type: String,
419    /// Size of one lot
420    #[serde(rename = "lotSize")]
421    pub lot_size: f64,
422    /// Highest price of the current trading session
423    pub high: Option<f64>,
424    /// Lowest price of the current trading session
425    pub low: Option<f64>,
426    /// Percentage change in price since previous close
427    #[serde(rename = "percentageChange")]
428    pub percentage_change: f64,
429    /// Net change in price since previous close
430    #[serde(rename = "netChange")]
431    pub net_change: f64,
432    /// Current bid price
433    pub bid: Option<f64>,
434    /// Current offer/ask price
435    pub offer: Option<f64>,
436    /// Time of the last price update
437    #[serde(rename = "updateTime")]
438    pub update_time: String,
439    /// UTC time of the last price update
440    #[serde(rename = "updateTimeUTC")]
441    pub update_time_utc: String,
442    /// Delay time in milliseconds for market data
443    #[serde(rename = "delayTime")]
444    pub delay_time: i64,
445    /// Whether streaming prices are available for this market
446    #[serde(rename = "streamingPricesAvailable")]
447    pub streaming_prices_available: bool,
448    /// Current status of the market (e.g., "OPEN", "CLOSED")
449    #[serde(rename = "marketStatus")]
450    pub market_status: String,
451    /// Factor for scaling prices
452    #[serde(rename = "scalingFactor")]
453    pub scaling_factor: i64,
454}
455
456/// Working orders
457#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
458pub struct WorkingOrders {
459    /// List of pending working orders
460    #[serde(rename = "workingOrders")]
461    pub working_orders: Vec<WorkingOrder>,
462}
463
464/// Working order
465#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
466pub struct WorkingOrder {
467    /// Details of the working order
468    #[serde(rename = "workingOrderData")]
469    pub working_order_data: WorkingOrderData,
470    /// Market information for the working order
471    #[serde(rename = "marketData")]
472    pub market_data: AccountMarketData,
473}
474
475/// Details of a working order
476#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
477pub struct WorkingOrderData {
478    /// Unique identifier for the deal
479    #[serde(rename = "dealId")]
480    pub deal_id: String,
481    /// Direction of the order (buy or sell)
482    pub direction: Direction,
483    /// Instrument EPIC identifier
484    pub epic: String,
485    /// Size/quantity of the order
486    #[serde(rename = "orderSize")]
487    pub order_size: f64,
488    /// Price level for the order
489    #[serde(rename = "orderLevel")]
490    pub order_level: f64,
491    /// Time in force for the order
492    #[serde(rename = "timeInForce")]
493    pub time_in_force: TimeInForce,
494    /// Expiry date for GTD orders
495    #[serde(rename = "goodTillDate")]
496    pub good_till_date: Option<String>,
497    /// ISO formatted expiry date for GTD orders
498    #[serde(rename = "goodTillDateISO")]
499    pub good_till_date_iso: Option<String>,
500    /// Date and time when the order was created
501    #[serde(rename = "createdDate")]
502    pub created_date: String,
503    /// UTC date and time when the order was created
504    #[serde(rename = "createdDateUTC")]
505    pub created_date_utc: String,
506    /// Whether the order has a guaranteed stop
507    #[serde(rename = "guaranteedStop")]
508    pub guaranteed_stop: bool,
509    /// Type of the order
510    #[serde(rename = "orderType")]
511    pub order_type: OrderType,
512    /// Distance for stop loss
513    #[serde(rename = "stopDistance")]
514    pub stop_distance: Option<f64>,
515    /// Distance for take profit
516    #[serde(rename = "limitDistance")]
517    pub limit_distance: Option<f64>,
518    /// Currency code for the order
519    #[serde(rename = "currencyCode")]
520    pub currency_code: String,
521    /// Whether direct market access is enabled
522    pub dma: bool,
523    /// Premium for limited risk
524    #[serde(rename = "limitedRiskPremium")]
525    pub limited_risk_premium: Option<f64>,
526    /// Price level for take profit
527    #[serde(rename = "limitLevel", default)]
528    pub limit_level: Option<f64>,
529    /// Price level for stop loss
530    #[serde(rename = "stopLevel", default)]
531    pub stop_level: Option<f64>,
532    /// Client-generated reference for the deal
533    #[serde(rename = "dealReference", default)]
534    pub deal_reference: Option<String>,
535}
536
537/// Market data for a working order
538#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
539pub struct AccountMarketData {
540    /// Human-readable name of the instrument
541    #[serde(rename = "instrumentName")]
542    pub instrument_name: String,
543    /// Exchange identifier
544    #[serde(rename = "exchangeId")]
545    pub exchange_id: String,
546    /// Expiry date of the instrument
547    pub expiry: String,
548    /// Current status of the market
549    #[serde(rename = "marketStatus")]
550    pub market_status: MarketState,
551    /// Unique identifier for the market
552    pub epic: String,
553    /// Type of the instrument
554    #[serde(rename = "instrumentType")]
555    pub instrument_type: InstrumentType,
556    /// Size of one lot
557    #[serde(rename = "lotSize")]
558    pub lot_size: f64,
559    /// Highest price of the current trading session
560    pub high: Option<f64>,
561    /// Lowest price of the current trading session
562    pub low: Option<f64>,
563    /// Percentage change in price since previous close
564    #[serde(rename = "percentageChange")]
565    pub percentage_change: f64,
566    /// Net change in price since previous close
567    #[serde(rename = "netChange")]
568    pub net_change: f64,
569    /// Current bid price
570    pub bid: Option<f64>,
571    /// Current offer/ask price
572    pub offer: Option<f64>,
573    /// Time of the last price update
574    #[serde(rename = "updateTime")]
575    pub update_time: String,
576    /// UTC time of the last price update
577    #[serde(rename = "updateTimeUTC")]
578    pub update_time_utc: String,
579    /// Delay time in milliseconds for market data
580    #[serde(rename = "delayTime")]
581    pub delay_time: i64,
582    /// Whether streaming prices are available for this market
583    #[serde(rename = "streamingPricesAvailable")]
584    pub streaming_prices_available: bool,
585    /// Factor for scaling prices
586    #[serde(rename = "scalingFactor")]
587    pub scaling_factor: i64,
588}
589
590/// Transaction history
591#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
592pub struct TransactionHistory {
593    /// List of account transactions
594    pub transactions: Vec<AccountTransaction>,
595    /// Metadata about the transaction list
596    pub metadata: TransactionMetadata,
597}
598
599/// Transaction metadata
600#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
601pub struct TransactionMetadata {
602    /// Pagination information
603    #[serde(rename = "pageData")]
604    pub page_data: PageData,
605    /// Total number of transactions
606    pub size: i32,
607}
608
609/// Pagination information
610#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
611pub struct PageData {
612    /// Current page number
613    #[serde(rename = "pageNumber")]
614    pub page_number: i32,
615    /// Number of items per page
616    #[serde(rename = "pageSize")]
617    pub page_size: i32,
618    /// Total number of pages
619    #[serde(rename = "totalPages")]
620    pub total_pages: i32,
621}
622
623/// Individual transaction
624#[derive(Debug, Clone, DisplaySimple, Deserialize, Serialize)]
625pub struct AccountTransaction {
626    /// Date and time of the transaction
627    pub date: String,
628    /// UTC date and time of the transaction
629    #[serde(rename = "dateUtc")]
630    pub date_utc: String,
631    /// Represents the date and time in UTC when an event or entity was opened or initiated.
632    #[serde(rename = "openDateUtc")]
633    pub open_date_utc: String,
634    /// Name of the instrument
635    #[serde(rename = "instrumentName")]
636    pub instrument_name: String,
637    /// Time period of the transaction
638    pub period: String,
639    /// Profit or loss amount
640    #[serde(rename = "profitAndLoss")]
641    pub profit_and_loss: String,
642    /// Type of transaction
643    #[serde(rename = "transactionType")]
644    pub transaction_type: String,
645    /// Reference identifier for the transaction
646    pub reference: String,
647    /// Opening price level
648    #[serde(rename = "openLevel")]
649    pub open_level: String,
650    /// Closing price level
651    #[serde(rename = "closeLevel")]
652    pub close_level: String,
653    /// Size/quantity of the transaction
654    pub size: String,
655    /// Currency of the transaction
656    pub currency: String,
657    /// Whether this is a cash transaction
658    #[serde(rename = "cashTransaction")]
659    pub cash_transaction: bool,
660}