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::impl_json_display;
9use crate::presentation::MarketState;
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, 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, 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, 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, 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
247impl_json_display!(ActionType);
248
249/// Action associated with an activity
250#[derive(Debug, Clone, Deserialize, Serialize)]
251pub struct ActivityAction {
252    /// Type of action
253    #[serde(rename = "actionType")]
254    pub action_type: ActionType,
255    /// Deal ID affected by this action
256    #[serde(rename = "affectedDealId", default)]
257    pub affected_deal_id: Option<String>,
258}
259
260/// Open positions
261#[derive(Debug, Clone, Deserialize, Serialize)]
262pub struct Positions {
263    /// List of open positions
264    pub positions: Vec<Position>,
265}
266
267impl Positions {
268    /// Compact positions by epic, combining positions with the same epic
269    ///
270    /// This method takes a vector of positions and returns a new vector where
271    /// positions with the same epic have been combined into a single position.
272    ///
273    /// # Arguments
274    /// * `positions` - A vector of positions to compact
275    ///
276    /// # Returns
277    /// A vector of positions with unique epics
278    pub fn compact_by_epic(positions: Vec<Position>) -> Vec<Position> {
279        let mut epic_map: HashMap<String, Position> = std::collections::HashMap::new();
280
281        for position in positions {
282            let epic = position.market.epic.clone();
283            epic_map
284                .entry(epic)
285                .and_modify(|existing| {
286                    *existing = existing.clone() + position.clone();
287                })
288                .or_insert(position);
289        }
290
291        epic_map.into_values().collect()
292    }
293}
294
295/// Individual position
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct Position {
298    /// Details of the position
299    pub position: PositionDetails,
300    /// Market information for the position
301    pub market: PositionMarket,
302    /// Profit and loss for the position
303    pub pnl: Option<f64>,
304}
305
306impl Add for Position {
307    type Output = Position;
308
309    fn add(self, other: Position) -> Position {
310        if self.market.epic != other.market.epic {
311            panic!("Cannot add positions from different markets");
312        }
313        Position {
314            position: self.position + other.position,
315            market: self.market,
316            pnl: match (self.pnl, other.pnl) {
317                (Some(a), Some(b)) => Some(a + b),
318                (Some(a), None) => Some(a),
319                (None, Some(b)) => Some(b),
320                (None, None) => None,
321            },
322        }
323    }
324}
325
326/// Details of a position
327#[derive(Debug, Clone, Deserialize, Serialize)]
328pub struct PositionDetails {
329    /// Size of one contract
330    #[serde(rename = "contractSize")]
331    pub contract_size: f64,
332    /// Date and time when the position was created
333    #[serde(rename = "createdDate")]
334    pub created_date: String,
335    /// UTC date and time when the position was created
336    #[serde(rename = "createdDateUTC")]
337    pub created_date_utc: String,
338    /// Unique identifier for the deal
339    #[serde(rename = "dealId")]
340    pub deal_id: String,
341    /// Client-generated reference for the deal
342    #[serde(rename = "dealReference")]
343    pub deal_reference: String,
344    /// Direction of the position (buy or sell)
345    pub direction: Direction,
346    /// Price level for take profit
347    #[serde(rename = "limitLevel")]
348    pub limit_level: Option<f64>,
349    /// Opening price level of the position
350    pub level: f64,
351    /// Size/quantity of the position
352    pub size: f64,
353    /// Price level for stop loss
354    #[serde(rename = "stopLevel")]
355    pub stop_level: Option<f64>,
356    /// Step size for trailing stop
357    #[serde(rename = "trailingStep")]
358    pub trailing_step: Option<f64>,
359    /// Distance for trailing stop
360    #[serde(rename = "trailingStopDistance")]
361    pub trailing_stop_distance: Option<f64>,
362    /// Currency of the position
363    pub currency: String,
364    /// Whether the position has controlled risk
365    #[serde(rename = "controlledRisk")]
366    pub controlled_risk: bool,
367    /// Premium paid for limited risk
368    #[serde(rename = "limitedRiskPremium")]
369    pub limited_risk_premium: Option<f64>,
370}
371
372impl Add for PositionDetails {
373    type Output = PositionDetails;
374
375    fn add(self, other: PositionDetails) -> PositionDetails {
376        let (contract_size, size) = if self.direction != other.direction {
377            (
378                (self.contract_size - other.contract_size).abs(),
379                (self.size - other.size).abs(),
380            )
381        } else {
382            (
383                self.contract_size + other.contract_size,
384                self.size + other.size,
385            )
386        };
387
388        PositionDetails {
389            contract_size,
390            created_date: self.created_date,
391            created_date_utc: self.created_date_utc,
392            deal_id: self.deal_id,
393            deal_reference: self.deal_reference,
394            direction: self.direction,
395            limit_level: other.limit_level.or(self.limit_level),
396            level: (self.level + other.level) / 2.0, // Average level
397            size,
398            stop_level: other.stop_level.or(self.stop_level),
399            trailing_step: other.trailing_step.or(self.trailing_step),
400            trailing_stop_distance: other.trailing_stop_distance.or(self.trailing_stop_distance),
401            currency: self.currency.clone(),
402            controlled_risk: self.controlled_risk || other.controlled_risk,
403            limited_risk_premium: other.limited_risk_premium.or(self.limited_risk_premium),
404        }
405    }
406}
407
408/// Market information for a position
409#[derive(Debug, Clone, Deserialize, Serialize)]
410pub struct PositionMarket {
411    /// Human-readable name of the instrument
412    #[serde(rename = "instrumentName")]
413    pub instrument_name: String,
414    /// Expiry date of the instrument
415    pub expiry: String,
416    /// Unique identifier for the market
417    pub epic: String,
418    /// Type of the instrument
419    #[serde(rename = "instrumentType")]
420    pub instrument_type: String,
421    /// Size of one lot
422    #[serde(rename = "lotSize")]
423    pub lot_size: f64,
424    /// Highest price of the current trading session
425    pub high: f64,
426    /// Lowest price of the current trading session
427    pub low: f64,
428    /// Percentage change in price since previous close
429    #[serde(rename = "percentageChange")]
430    pub percentage_change: f64,
431    /// Net change in price since previous close
432    #[serde(rename = "netChange")]
433    pub net_change: f64,
434    /// Current bid price
435    pub bid: f64,
436    /// Current offer/ask price
437    pub offer: f64,
438    /// Time of the last price update
439    #[serde(rename = "updateTime")]
440    pub update_time: String,
441    /// UTC time of the last price update
442    #[serde(rename = "updateTimeUTC")]
443    pub update_time_utc: String,
444    /// Delay time in milliseconds for market data
445    #[serde(rename = "delayTime")]
446    pub delay_time: i64,
447    /// Whether streaming prices are available for this market
448    #[serde(rename = "streamingPricesAvailable")]
449    pub streaming_prices_available: bool,
450    /// Current status of the market (e.g., "OPEN", "CLOSED")
451    #[serde(rename = "marketStatus")]
452    pub market_status: String,
453    /// Factor for scaling prices
454    #[serde(rename = "scalingFactor")]
455    pub scaling_factor: i64,
456}
457
458/// Working orders
459#[derive(Debug, Clone, Deserialize, Serialize)]
460pub struct WorkingOrders {
461    /// List of pending working orders
462    #[serde(rename = "workingOrders")]
463    pub working_orders: Vec<WorkingOrder>,
464}
465
466/// Working order
467#[derive(Debug, Clone, Deserialize, Serialize)]
468pub struct WorkingOrder {
469    /// Details of the working order
470    #[serde(rename = "workingOrderData")]
471    pub working_order_data: WorkingOrderData,
472    /// Market information for the working order
473    #[serde(rename = "marketData")]
474    pub market_data: MarketData,
475}
476
477/// Details of a working order
478#[derive(Debug, Clone, Deserialize, Serialize)]
479pub struct WorkingOrderData {
480    /// Unique identifier for the deal
481    #[serde(rename = "dealId")]
482    pub deal_id: String,
483    /// Direction of the order (buy or sell)
484    pub direction: Direction,
485    /// Instrument EPIC identifier
486    pub epic: String,
487    /// Size/quantity of the order
488    #[serde(rename = "orderSize")]
489    pub order_size: f64,
490    /// Price level for the order
491    #[serde(rename = "orderLevel")]
492    pub order_level: f64,
493    /// Time in force for the order
494    #[serde(rename = "timeInForce")]
495    pub time_in_force: TimeInForce,
496    /// Expiry date for GTD orders
497    #[serde(rename = "goodTillDate")]
498    pub good_till_date: Option<String>,
499    /// ISO formatted expiry date for GTD orders
500    #[serde(rename = "goodTillDateISO")]
501    pub good_till_date_iso: Option<String>,
502    /// Date and time when the order was created
503    #[serde(rename = "createdDate")]
504    pub created_date: String,
505    /// UTC date and time when the order was created
506    #[serde(rename = "createdDateUTC")]
507    pub created_date_utc: String,
508    /// Whether the order has a guaranteed stop
509    #[serde(rename = "guaranteedStop")]
510    pub guaranteed_stop: bool,
511    /// Type of the order
512    #[serde(rename = "orderType")]
513    pub order_type: OrderType,
514    /// Distance for stop loss
515    #[serde(rename = "stopDistance")]
516    pub stop_distance: Option<f64>,
517    /// Distance for take profit
518    #[serde(rename = "limitDistance")]
519    pub limit_distance: Option<f64>,
520    /// Currency code for the order
521    #[serde(rename = "currencyCode")]
522    pub currency_code: String,
523    /// Whether direct market access is enabled
524    pub dma: bool,
525    /// Premium for limited risk
526    #[serde(rename = "limitedRiskPremium")]
527    pub limited_risk_premium: Option<f64>,
528    /// Price level for take profit
529    #[serde(rename = "limitLevel", default)]
530    pub limit_level: Option<f64>,
531    /// Price level for stop loss
532    #[serde(rename = "stopLevel", default)]
533    pub stop_level: Option<f64>,
534    /// Client-generated reference for the deal
535    #[serde(rename = "dealReference", default)]
536    pub deal_reference: Option<String>,
537}
538
539/// Market data for a working order
540#[derive(Debug, Clone, Deserialize, Serialize)]
541pub struct MarketData {
542    /// Human-readable name of the instrument
543    #[serde(rename = "instrumentName")]
544    pub instrument_name: String,
545    /// Exchange identifier
546    #[serde(rename = "exchangeId")]
547    pub exchange_id: String,
548    /// Expiry date of the instrument
549    pub expiry: String,
550    /// Current status of the market
551    #[serde(rename = "marketStatus")]
552    pub market_status: MarketState,
553    /// Unique identifier for the market
554    pub epic: String,
555    /// Type of the instrument
556    #[serde(rename = "instrumentType")]
557    pub instrument_type: InstrumentType,
558    /// Size of one lot
559    #[serde(rename = "lotSize")]
560    pub lot_size: f64,
561    /// Highest price of the current trading session
562    pub high: f64,
563    /// Lowest price of the current trading session
564    pub low: f64,
565    /// Percentage change in price since previous close
566    #[serde(rename = "percentageChange")]
567    pub percentage_change: f64,
568    /// Net change in price since previous close
569    #[serde(rename = "netChange")]
570    pub net_change: f64,
571    /// Current bid price
572    pub bid: f64,
573    /// Current offer/ask price
574    pub offer: f64,
575    /// Time of the last price update
576    #[serde(rename = "updateTime")]
577    pub update_time: String,
578    /// UTC time of the last price update
579    #[serde(rename = "updateTimeUTC")]
580    pub update_time_utc: String,
581    /// Delay time in milliseconds for market data
582    #[serde(rename = "delayTime")]
583    pub delay_time: i64,
584    /// Whether streaming prices are available for this market
585    #[serde(rename = "streamingPricesAvailable")]
586    pub streaming_prices_available: bool,
587    /// Factor for scaling prices
588    #[serde(rename = "scalingFactor")]
589    pub scaling_factor: i64,
590}
591
592/// Transaction history
593#[derive(Debug, Clone, Deserialize, Serialize)]
594pub struct TransactionHistory {
595    /// List of account transactions
596    pub transactions: Vec<AccountTransaction>,
597    /// Metadata about the transaction list
598    pub metadata: TransactionMetadata,
599}
600
601/// Transaction metadata
602#[derive(Debug, Clone, Deserialize, Serialize)]
603pub struct TransactionMetadata {
604    /// Pagination information
605    #[serde(rename = "pageData")]
606    pub page_data: PageData,
607    /// Total number of transactions
608    pub size: i32,
609}
610
611/// Pagination information
612#[derive(Debug, Clone, Deserialize, Serialize)]
613pub struct PageData {
614    /// Current page number
615    #[serde(rename = "pageNumber")]
616    pub page_number: i32,
617    /// Number of items per page
618    #[serde(rename = "pageSize")]
619    pub page_size: i32,
620    /// Total number of pages
621    #[serde(rename = "totalPages")]
622    pub total_pages: i32,
623}
624
625/// Individual transaction
626#[derive(Debug, Clone, Deserialize, Serialize)]
627pub struct AccountTransaction {
628    /// Date and time of the transaction
629    pub date: String,
630    /// UTC date and time of the transaction
631    #[serde(rename = "dateUtc")]
632    pub date_utc: String,
633    /// Represents the date and time in UTC when an event or entity was opened or initiated.
634    #[serde(rename = "openDateUtc")]
635    pub open_date_utc: String,
636    /// Name of the instrument
637    #[serde(rename = "instrumentName")]
638    pub instrument_name: String,
639    /// Time period of the transaction
640    pub period: String,
641    /// Profit or loss amount
642    #[serde(rename = "profitAndLoss")]
643    pub profit_and_loss: String,
644    /// Type of transaction
645    #[serde(rename = "transactionType")]
646    pub transaction_type: String,
647    /// Reference identifier for the transaction
648    pub reference: String,
649    /// Opening price level
650    #[serde(rename = "openLevel")]
651    pub open_level: String,
652    /// Closing price level
653    #[serde(rename = "closeLevel")]
654    pub close_level: String,
655    /// Size/quantity of the transaction
656    pub size: String,
657    /// Currency of the transaction
658    pub currency: String,
659    /// Whether this is a cash transaction
660    #[serde(rename = "cashTransaction")]
661    pub cash_transaction: bool,
662}
663
664impl_json_display!(
665    Positions,
666    Position,
667    AccountTransaction,
668    MarketData,
669    PositionDetails,
670    PositionMarket
671);