ig_client/application/models/
order.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 13/5/25
5******************************************************************************/
6use crate::impl_json_display;
7use serde::{Deserialize, Deserializer, Serialize};
8
9const DEFAULT_ORDER_SELL_SIZE: f64 = 0.0;
10const DEFAULT_ORDER_BUY_SIZE: f64 = 10000.0;
11
12/// Order direction (buy or sell)
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
14#[serde(rename_all = "UPPERCASE")]
15pub enum Direction {
16    /// Buy direction (long position)
17    #[default]
18    Buy,
19    /// Sell direction (short position)
20    Sell,
21}
22
23impl_json_display!(Direction);
24
25/// Order type
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
27#[serde(rename_all = "UPPERCASE")]
28pub enum OrderType {
29    /// Limit order - executed when price reaches specified level
30    #[default]
31    Limit,
32    /// Market order - executed immediately at current market price
33    Market,
34    /// Quote order - executed at quoted price
35    Quote,
36    /// Stop order - becomes market order when price reaches specified level
37    Stop,
38    /// Stop limit order - becomes limit order when price reaches specified level
39    StopLimit,
40}
41
42/// Represents the status of an order or transaction in the system.
43///
44/// This enum covers various states an order can be in throughout its lifecycle,
45/// from creation to completion or cancellation.
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
47#[serde(rename_all = "UPPERCASE")]
48pub enum Status {
49    /// Order has been amended or modified after initial creation
50    Amended,
51    /// Order has been deleted from the system
52    Deleted,
53    /// Order has been completely closed with all positions resolved
54    #[serde(rename = "FULLY_CLOSED")]
55    FullyClosed,
56    /// Order has been opened and is active in the market
57    Opened,
58    /// Order has been partially closed with some positions still open
59    #[serde(rename = "PARTIALLY_CLOSED")]
60    PartiallyClosed,
61    /// Order has been closed but may differ from FullyClosed in context
62    Closed,
63    /// Default state - order is open and active in the market
64    #[default]
65    Open,
66    /// Order has been updated with new parameters
67    Updated,
68    /// Order has been accepted by the system or exchange
69    Accepted,
70    /// Order has been rejected by the system or exchange
71    Rejected,
72    /// Order is currently working (waiting to be filled)
73    Working,
74    /// Order has been filled (executed)
75    Filled,
76    /// Order has been cancelled
77    Cancelled,
78    /// Order has expired (time in force elapsed)
79    Expired,
80}
81
82/// Order duration (time in force)
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
84pub enum TimeInForce {
85    /// Order remains valid until cancelled by the client
86    #[serde(rename = "GOOD_TILL_CANCELLED")]
87    #[default]
88    GoodTillCancelled,
89    /// Order remains valid until a specified date
90    #[serde(rename = "GOOD_TILL_DATE")]
91    GoodTillDate,
92    /// Order is executed immediately (partially or completely) or cancelled
93    #[serde(rename = "IMMEDIATE_OR_CANCEL")]
94    ImmediateOrCancel,
95    /// Order must be filled completely immediately or cancelled
96    #[serde(rename = "FILL_OR_KILL")]
97    FillOrKill,
98}
99
100/// Model for creating a new order
101#[derive(Debug, Clone, Serialize)]
102pub struct CreateOrderRequest {
103    /// Instrument EPIC identifier
104    pub epic: String,
105    /// Order direction (buy or sell)
106    pub direction: Direction,
107    /// Order size/quantity
108    pub size: f64,
109    /// Type of order (market, limit, etc.)
110    #[serde(rename = "orderType")]
111    pub order_type: OrderType,
112    /// Order duration (how long the order remains valid)
113    #[serde(rename = "timeInForce")]
114    pub time_in_force: TimeInForce,
115    /// Price level for limit orders
116    #[serde(rename = "level", skip_serializing_if = "Option::is_none")]
117    pub level: Option<f64>,
118    /// Whether to use a guaranteed stop
119    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
120    pub guaranteed_stop: Option<bool>,
121    /// Price level for stop loss
122    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
123    pub stop_level: Option<f64>,
124    /// Distance for stop loss
125    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
126    pub stop_distance: Option<f64>,
127    /// Price level for take profit
128    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
129    pub limit_level: Option<f64>,
130    /// Distance for take profit
131    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
132    pub limit_distance: Option<f64>,
133    /// Expiry date for the order
134    #[serde(rename = "expiry", skip_serializing_if = "Option::is_none")]
135    pub expiry: Option<String>,
136    /// Client-generated reference for the deal
137    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
138    pub deal_reference: Option<String>,
139    /// Whether to force open a new position
140    #[serde(rename = "forceOpen", skip_serializing_if = "Option::is_none")]
141    pub force_open: Option<bool>,
142    /// Currency code for the order (e.g., "USD", "EUR")
143    #[serde(rename = "currencyCode", skip_serializing_if = "Option::is_none")]
144    pub currency_code: Option<String>,
145}
146
147impl CreateOrderRequest {
148    /// Creates a new market order
149    pub fn market(epic: String, direction: Direction, size: f64) -> Self {
150        Self {
151            epic,
152            direction,
153            size,
154            order_type: OrderType::Market,
155            time_in_force: TimeInForce::FillOrKill,
156            level: None,
157            guaranteed_stop: None,
158            stop_level: None,
159            stop_distance: None,
160            limit_level: None,
161            limit_distance: None,
162            expiry: None,
163            deal_reference: None,
164            force_open: Some(true),
165            currency_code: None,
166        }
167    }
168
169    /// Creates a new limit order
170    pub fn limit(epic: String, direction: Direction, size: f64, level: f64) -> Self {
171        Self {
172            epic,
173            direction,
174            size,
175            order_type: OrderType::Limit,
176            time_in_force: TimeInForce::GoodTillCancelled,
177            level: Some(level),
178            guaranteed_stop: None,
179            stop_level: None,
180            stop_distance: None,
181            limit_level: None,
182            limit_distance: None,
183            expiry: None,
184            deal_reference: None,
185            force_open: Some(true),
186            currency_code: None,
187        }
188    }
189
190    /// Creates a new instance of a market sell option with predefined parameters.
191    ///
192    /// This function sets up a sell option to the market for a given asset (`epic`)
193    /// with the specified size. It configures the order with default values
194    /// for attributes such as direction, order type, and time-in-force.
195    ///
196    /// # Parameters
197    /// - `epic`: A `String` that represents the epic (unique identifier or code) of the instrument
198    ///   being traded.
199    /// - `size`: A `f64` value representing the size or quantity of the order.
200    ///
201    /// # Returns
202    /// An instance of `Self` (the type implementing this function), containing the specified
203    /// `epic` and `size`, along with default values for other parameters:
204    ///
205    /// - `direction`: Set to `Direction::Sell`.
206    /// - `order_type`: Set to `OrderType::Limit`.
207    /// - `time_in_force`: Set to `TimeInForce::FillOrKill`.
208    /// - `level`: Set to `Some(0.1)`.
209    /// - `guaranteed_stop`: Set to `None`.
210    /// - `stop_level`: Set to `None`.
211    /// - `stop_distance`: Set to `None`.
212    /// - `limit_level`: Set to `None`.
213    /// - `limit_distance`: Set to `None`.
214    /// - `expiry`: Set to `None`.
215    /// - `deal_reference`: Set to `None`.
216    /// - `force_open`: Set to `Some(true)`.
217    /// - `currency_code`: Set to `None`.
218    ///
219    /// Note that this function allows for minimal input (the instrument and size),
220    /// while other fields are provided default values. If further customization is required,
221    /// you can modify the returned instance as needed.
222    #[allow(clippy::ptr_arg)]
223    pub fn sell_option_to_market(
224        epic: &String,
225        size: &f64,
226        expiry: &Option<String>,
227        deal_reference: &Option<String>,
228        currency_code: &Option<String>,
229    ) -> Self {
230        let rounded_size = (size * 100.0).floor() / 100.0;
231        let currency_code = if let Some(code) = currency_code {
232            Some(code.clone())
233        } else {
234            Some("EUR".to_string())
235        };
236
237        Self {
238            epic: epic.clone(),
239            direction: Direction::Sell,
240            size: rounded_size,
241            order_type: OrderType::Limit,
242            time_in_force: TimeInForce::FillOrKill,
243            level: Some(DEFAULT_ORDER_SELL_SIZE),
244            guaranteed_stop: Some(false),
245            stop_level: None,
246            stop_distance: None,
247            limit_level: None,
248            limit_distance: None,
249            expiry: expiry.clone(),
250            deal_reference: deal_reference.clone(),
251            force_open: Some(true),
252            currency_code,
253        }
254    }
255
256    /// Creates a new instance of an order to buy an option in the market with specified parameters.
257    ///
258    /// This method initializes an order with the following default values:
259    /// - `direction` is set to `Buy`.
260    /// - `order_type` is set to `Limit`.
261    /// - `time_in_force` is set to `FillOrKill`.
262    /// - `level` is set to `Some(10000.0)`.
263    /// - `force_open` is set to `Some(true)`.
264    ///   Other optional parameters, such as stop levels, distances, expiry, and currency code, are left as `None`.
265    ///
266    /// # Parameters
267    /// - `epic` (`String`): The identifier for the market or instrument to trade.
268    /// - `size` (`f64`): The size or quantity of the order to be executed.
269    ///
270    /// # Returns
271    /// A new instance of `Self` that represents the configured buy option for the given market.
272    ///
273    /// # Note
274    /// Ensure the `epic` and `size` values provided are valid and match required market conditions.
275    #[allow(clippy::ptr_arg)]
276    pub fn buy_option_to_market(
277        epic: &String,
278        size: &f64,
279        expiry: &Option<String>,
280        deal_reference: &Option<String>,
281        currency_code: &Option<String>,
282    ) -> Self {
283        let rounded_size = (size * 100.0).floor() / 100.0;
284        let currency_code = if let Some(code) = currency_code {
285            Some(code.clone())
286        } else {
287            Some("EUR".to_string())
288        };
289        Self {
290            epic: epic.clone(),
291            direction: Direction::Buy,
292            size: rounded_size,
293            order_type: OrderType::Limit,
294            time_in_force: TimeInForce::FillOrKill,
295            level: Some(DEFAULT_ORDER_BUY_SIZE),
296            guaranteed_stop: Some(false),
297            stop_level: None,
298            stop_distance: None,
299            limit_level: None,
300            limit_distance: None,
301            expiry: expiry.clone(),
302            deal_reference: deal_reference.clone(),
303            force_open: Some(true),
304            currency_code: currency_code.clone(),
305        }
306    }
307
308    /// Adds a stop loss to the order
309    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
310        self.stop_level = Some(stop_level);
311        self
312    }
313
314    /// Adds a take profit to the order
315    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
316        self.limit_level = Some(limit_level);
317        self
318    }
319
320    /// Adds a reference to the order
321    pub fn with_reference(mut self, reference: String) -> Self {
322        self.deal_reference = Some(reference);
323        self
324    }
325}
326
327/// Response to order creation
328#[derive(Debug, Clone, Deserialize)]
329pub struct CreateOrderResponse {
330    /// Client-generated reference for the deal
331    #[serde(rename = "dealReference")]
332    pub deal_reference: String,
333}
334
335/// Helper function to deserialize a nullable status field
336/// When the status is null in the JSON, we default to Rejected status
337fn deserialize_nullable_status<'de, D>(deserializer: D) -> Result<Status, D::Error>
338where
339    D: Deserializer<'de>,
340{
341    let opt = Option::deserialize(deserializer)?;
342    Ok(opt.unwrap_or(Status::Rejected))
343}
344
345/// Details of a confirmed order
346#[derive(Debug, Clone, Deserialize)]
347pub struct OrderConfirmation {
348    /// Date and time of the confirmation
349    pub date: String,
350    /// Status of the order (accepted, rejected, etc.)
351    /// This can be null in some responses (e.g., when market is closed)
352    #[serde(deserialize_with = "deserialize_nullable_status")]
353    pub status: Status,
354    /// Reason for rejection if applicable
355    pub reason: Option<String>,
356    /// Unique identifier for the deal
357    #[serde(rename = "dealId")]
358    pub deal_id: Option<String>,
359    /// Client-generated reference for the deal
360    #[serde(rename = "dealReference")]
361    pub deal_reference: String,
362    /// Status of the deal
363    #[serde(rename = "dealStatus")]
364    pub deal_status: Option<String>,
365    /// Instrument EPIC identifier
366    pub epic: Option<String>,
367    /// Expiry date for the order
368    #[serde(rename = "expiry")]
369    pub expiry: Option<String>,
370    /// Whether a guaranteed stop was used
371    #[serde(rename = "guaranteedStop")]
372    pub guaranteed_stop: Option<bool>,
373    /// Price level of the order
374    #[serde(rename = "level")]
375    pub level: Option<f64>,
376    /// Distance for take profit
377    #[serde(rename = "limitDistance")]
378    pub limit_distance: Option<f64>,
379    /// Price level for take profit
380    #[serde(rename = "limitLevel")]
381    pub limit_level: Option<f64>,
382    /// Size/quantity of the order
383    pub size: Option<f64>,
384    /// Distance for stop loss
385    #[serde(rename = "stopDistance")]
386    pub stop_distance: Option<f64>,
387    /// Price level for stop loss
388    #[serde(rename = "stopLevel")]
389    pub stop_level: Option<f64>,
390    /// Whether a trailing stop was used
391    #[serde(rename = "trailingStop")]
392    pub trailing_stop: Option<bool>,
393    /// Direction of the order (buy or sell)
394    pub direction: Option<Direction>,
395}
396
397/// Model for updating an existing position
398#[derive(Debug, Clone, Serialize)]
399pub struct UpdatePositionRequest {
400    /// New price level for stop loss
401    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
402    pub stop_level: Option<f64>,
403    /// New price level for take profit
404    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
405    pub limit_level: Option<f64>,
406    /// Whether to enable trailing stop
407    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
408    pub trailing_stop: Option<bool>,
409    /// Distance for trailing stop
410    #[serde(
411        rename = "trailingStopDistance",
412        skip_serializing_if = "Option::is_none"
413    )]
414    pub trailing_stop_distance: Option<f64>,
415}
416
417/// Model for closing an existing position
418#[derive(Debug, Clone, Serialize)]
419pub struct ClosePositionRequest {
420    /// Unique identifier for the position to close
421    #[serde(rename = "dealId")]
422    pub deal_id: Option<String>,
423    /// Direction of the closing order (opposite to the position)
424    pub direction: Direction,
425    /// Size/quantity to close
426    pub size: f64,
427    /// Type of order to use for closing
428    #[serde(rename = "orderType")]
429    pub order_type: OrderType,
430    /// Order duration for the closing order
431    #[serde(rename = "timeInForce")]
432    pub time_in_force: TimeInForce,
433    /// Price level for limit close orders
434    #[serde(rename = "level", skip_serializing_if = "Option::is_none")]
435    pub level: Option<f64>,
436    /// Expiry date for the order
437    #[serde(rename = "expiry")]
438    pub expiry: Option<String>,
439    /// Instrument EPIC identifier
440    pub epic: Option<String>,
441
442    /// Quote identifier for the order, used for certain order types that require a specific quote
443    #[serde(rename = "quoteId")]
444    pub quote_id: Option<String>,
445}
446
447impl ClosePositionRequest {
448    /// Creates a request to close a position at market price
449    pub fn market(deal_id: String, direction: Direction, size: f64) -> Self {
450        Self {
451            deal_id: Some(deal_id),
452            direction,
453            size,
454            order_type: OrderType::Market,
455            time_in_force: TimeInForce::FillOrKill,
456            level: None,
457            expiry: None,
458            epic: None,
459            quote_id: None,
460        }
461    }
462
463    /// Creates a request to close a position at a specific price level
464    ///
465    /// This is useful for instruments that don't support market orders
466    pub fn limit(deal_id: String, direction: Direction, size: f64, level: f64) -> Self {
467        Self {
468            deal_id: Some(deal_id),
469            direction,
470            size,
471            order_type: OrderType::Limit,
472            time_in_force: TimeInForce::FillOrKill,
473            level: Some(level),
474            expiry: None,
475            epic: None,
476            quote_id: None,
477        }
478    }
479
480    /// Creates a request to close an option position by deal ID using a limit order with predefined price levels
481    ///
482    /// This is specifically designed for options trading where market orders are not supported
483    /// and a limit order with a predefined price level is required based on the direction.
484    ///
485    /// # Arguments
486    /// * `deal_id` - The ID of the deal to close
487    /// * `direction` - The direction of the closing order (opposite of the position direction)
488    /// * `size` - The size of the position to close
489    pub fn close_option_to_market_by_id(deal_id: String, direction: Direction, size: f64) -> Self {
490        let level = match direction {
491            Direction::Buy => Some(DEFAULT_ORDER_BUY_SIZE),
492            Direction::Sell => Some(DEFAULT_ORDER_SELL_SIZE),
493        };
494        Self {
495            deal_id: Some(deal_id),
496            direction,
497            size,
498            order_type: OrderType::Limit,
499            time_in_force: TimeInForce::FillOrKill,
500            level,
501            expiry: None,
502            epic: None,
503            quote_id: None,
504        }
505    }
506
507    /// Creates a request to close an option position by epic identifier using a limit order with predefined price levels
508    ///
509    /// This is specifically designed for options trading where market orders are not supported
510    /// and a limit order with a predefined price level is required based on the direction.
511    /// This method is used when the deal ID is not available but the epic and expiry are known.
512    ///
513    /// # Arguments
514    /// * `epic` - The epic identifier of the instrument
515    /// * `expiry` - The expiry date of the option
516    /// * `direction` - The direction of the closing order (opposite of the position direction)
517    /// * `size` - The size of the position to close
518    pub fn close_option_to_market_by_epic(
519        epic: String,
520        expiry: String,
521        direction: Direction,
522        size: f64,
523    ) -> Self {
524        let level = match direction {
525            Direction::Buy => Some(DEFAULT_ORDER_BUY_SIZE),
526            Direction::Sell => Some(DEFAULT_ORDER_SELL_SIZE),
527        };
528        Self {
529            deal_id: None,
530            direction,
531            size,
532            order_type: OrderType::Limit,
533            time_in_force: TimeInForce::FillOrKill,
534            level,
535            expiry: Some(expiry),
536            epic: Some(epic),
537            quote_id: None,
538        }
539    }
540}
541
542/// Response to closing a position
543#[derive(Debug, Clone, Deserialize)]
544pub struct ClosePositionResponse {
545    /// Client-generated reference for the closing deal
546    #[serde(rename = "dealReference")]
547    pub deal_reference: String,
548}
549
550/// Response to updating a position
551#[derive(Debug, Clone, Deserialize)]
552pub struct UpdatePositionResponse {
553    /// Client-generated reference for the update deal
554    #[serde(rename = "dealReference")]
555    pub deal_reference: String,
556}
557
558/// Model for creating a new working order
559#[derive(Debug, Clone, Serialize)]
560pub struct CreateWorkingOrderRequest {
561    /// Instrument EPIC identifier
562    pub epic: String,
563    /// Order direction (buy or sell)
564    pub direction: Direction,
565    /// Order size/quantity
566    pub size: f64,
567    /// Price level for the order
568    pub level: f64,
569    /// Type of working order (LIMIT or STOP)
570    #[serde(rename = "type")]
571    pub order_type: OrderType,
572    /// Order duration (how long the order remains valid)
573    #[serde(rename = "timeInForce")]
574    pub time_in_force: TimeInForce,
575    /// Whether to use a guaranteed stop
576    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
577    pub guaranteed_stop: Option<bool>,
578    /// Price level for stop loss
579    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
580    pub stop_level: Option<f64>,
581    /// Distance for stop loss
582    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
583    pub stop_distance: Option<f64>,
584    /// Price level for take profit
585    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
586    pub limit_level: Option<f64>,
587    /// Distance for take profit
588    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
589    pub limit_distance: Option<f64>,
590    /// Expiry date for GTD orders
591    #[serde(rename = "goodTillDate", skip_serializing_if = "Option::is_none")]
592    pub good_till_date: Option<String>,
593    /// Client-generated reference for the deal
594    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
595    pub deal_reference: Option<String>,
596    /// Currency code for the order (e.g., "USD", "EUR")
597    #[serde(rename = "currencyCode", skip_serializing_if = "Option::is_none")]
598    pub currency_code: Option<String>,
599}
600
601impl CreateWorkingOrderRequest {
602    /// Creates a new limit working order
603    pub fn limit(epic: String, direction: Direction, size: f64, level: f64) -> Self {
604        Self {
605            epic,
606            direction,
607            size,
608            level,
609            order_type: OrderType::Limit,
610            time_in_force: TimeInForce::GoodTillCancelled,
611            guaranteed_stop: None,
612            stop_level: None,
613            stop_distance: None,
614            limit_level: None,
615            limit_distance: None,
616            good_till_date: None,
617            deal_reference: None,
618            currency_code: None,
619        }
620    }
621
622    /// Creates a new stop working order
623    pub fn stop(epic: String, direction: Direction, size: f64, level: f64) -> Self {
624        Self {
625            epic,
626            direction,
627            size,
628            level,
629            order_type: OrderType::Stop,
630            time_in_force: TimeInForce::GoodTillCancelled,
631            guaranteed_stop: None,
632            stop_level: None,
633            stop_distance: None,
634            limit_level: None,
635            limit_distance: None,
636            good_till_date: None,
637            deal_reference: None,
638            currency_code: None,
639        }
640    }
641
642    /// Adds a stop loss to the working order
643    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
644        self.stop_level = Some(stop_level);
645        self
646    }
647
648    /// Adds a take profit to the working order
649    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
650        self.limit_level = Some(limit_level);
651        self
652    }
653
654    /// Adds a reference to the working order
655    pub fn with_reference(mut self, reference: String) -> Self {
656        self.deal_reference = Some(reference);
657        self
658    }
659
660    /// Sets the order to expire at a specific date
661    pub fn expires_at(mut self, date: String) -> Self {
662        self.time_in_force = TimeInForce::GoodTillDate;
663        self.good_till_date = Some(date);
664        self
665    }
666}
667
668/// Response to working order creation
669#[derive(Debug, Clone, Deserialize)]
670pub struct CreateWorkingOrderResponse {
671    /// Client-generated reference for the deal
672    #[serde(rename = "dealReference")]
673    pub deal_reference: String,
674}