Skip to main content

ig_client/model/
requests.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 19/10/25
5******************************************************************************/
6use crate::constants::{DEFAULT_ORDER_BUY_LEVEL, DEFAULT_ORDER_SELL_LEVEL};
7use crate::presentation::account::WorkingOrder;
8use crate::presentation::order::{Direction, OrderType, TimeInForce};
9use chrono::{Duration, Utc};
10use pretty_simple_display::{DebugPretty, DisplaySimple};
11use serde::{Deserialize, Serialize};
12use std::fmt;
13use std::fmt::{Debug, Display};
14
15/// Parameters for getting recent prices (API v3)
16#[derive(Clone, Default, Deserialize, Serialize)]
17pub struct RecentPricesRequest<'a> {
18    /// Instrument epic
19    pub epic: &'a str,
20    /// Optional price resolution (default: MINUTE)
21    pub resolution: Option<&'a str>,
22    /// Optional start date time (yyyy-MM-dd'T'HH:mm:ss)
23    pub from: Option<&'a str>,
24    /// Optional end date time (yyyy-MM-dd'T'HH:mm:ss)
25    pub to: Option<&'a str>,
26    /// Optional max number of price points (default: 10)
27    #[serde(rename = "max")]
28    pub max_points: Option<u32>,
29    /// Optional page size (default: 20, disable paging = 0)
30    #[serde(rename = "pageSize")]
31    pub page_size: Option<u32>,
32    /// Optional page number (default: 1)
33    #[serde(rename = "pageNumber")]
34    pub page_number: Option<u32>,
35}
36
37impl<'a> RecentPricesRequest<'a> {
38    /// Create new parameters with just the epic (required field)
39    #[must_use]
40    pub fn new(epic: &'a str) -> Self {
41        Self {
42            epic,
43            ..Default::default()
44        }
45    }
46
47    /// Set the resolution
48    #[must_use]
49    pub fn with_resolution(mut self, resolution: &'a str) -> Self {
50        self.resolution = Some(resolution);
51        self
52    }
53
54    /// Set the from date
55    #[must_use]
56    pub fn with_from(mut self, from: &'a str) -> Self {
57        self.from = Some(from);
58        self
59    }
60
61    /// Set the to date
62    #[must_use]
63    pub fn with_to(mut self, to: &'a str) -> Self {
64        self.to = Some(to);
65        self
66    }
67
68    /// Set the max points
69    #[must_use]
70    pub fn with_max_points(mut self, max_points: u32) -> Self {
71        self.max_points = Some(max_points);
72        self
73    }
74
75    /// Set the page size
76    #[must_use]
77    pub fn with_page_size(mut self, page_size: u32) -> Self {
78        self.page_size = Some(page_size);
79        self
80    }
81
82    /// Set the page number
83    #[must_use]
84    pub fn with_page_number(mut self, page_number: u32) -> Self {
85        self.page_number = Some(page_number);
86        self
87    }
88}
89
90impl Display for RecentPricesRequest<'_> {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        let json = serde_json::to_string(self).unwrap_or_else(|_| "Invalid JSON".to_string());
93        write!(f, "{}", json)
94    }
95}
96
97impl Debug for RecentPricesRequest<'_> {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        let json =
100            serde_json::to_string_pretty(self).unwrap_or_else(|_| "Invalid JSON".to_string());
101        write!(f, "{}", json)
102    }
103}
104
105/// Model for creating a new order
106#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
107pub struct CreateOrderRequest {
108    /// Instrument EPIC identifier
109    pub epic: String,
110    /// Order direction (buy or sell)
111    pub direction: Direction,
112    /// Order size/quantity
113    pub size: f64,
114    /// Type of order (market, limit, etc.)
115    #[serde(rename = "orderType")]
116    pub order_type: OrderType,
117    /// Order duration (how long the order remains valid)
118    #[serde(rename = "timeInForce")]
119    pub time_in_force: TimeInForce,
120    /// Price level for limit orders
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub level: Option<f64>,
123    /// Whether to use a guaranteed stop
124    #[serde(rename = "guaranteedStop")]
125    pub guaranteed_stop: bool,
126    /// Price level for stop loss
127    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
128    pub stop_level: Option<f64>,
129    /// Stop loss distance
130    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
131    pub stop_distance: Option<f64>,
132    /// Price level for take profit
133    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
134    pub limit_level: Option<f64>,
135    /// Take profit distance
136    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
137    pub limit_distance: Option<f64>,
138    /// Expiry date for the order
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub expiry: Option<String>,
141    /// Client-generated reference for the deal
142    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
143    pub deal_reference: Option<String>,
144    /// Whether to force open a new position
145    #[serde(rename = "forceOpen")]
146    pub force_open: bool,
147    /// Currency code for the order (e.g., "USD", "EUR")
148    #[serde(rename = "currencyCode")]
149    pub currency_code: String,
150    /// Quote identifier for the order
151    #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")]
152    pub quote_id: Option<String>,
153    /// Trailing stop enabled
154    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
155    pub trailing_stop: Option<bool>,
156    /// Trailing stop increment (only if trailingStop is true)
157    #[serde(
158        rename = "trailingStopIncrement",
159        skip_serializing_if = "Option::is_none"
160    )]
161    pub trailing_stop_increment: Option<f64>,
162}
163
164/// Rounds an order size to two decimal places (nearest, half away from zero).
165///
166/// IG accepts order sizes to two decimals. Rounding must go through the nearest
167/// value, not `floor`: `0.29 * 100.0` is `28.999999999999996` in `f64`, so a
168/// `floor`-based `(size * 100.0).floor() / 100.0` silently yields `0.28` and the
169/// order is placed with a smaller size than the caller requested. `round` maps
170/// it back to `0.29`. The rounding direction (nearest) is deliberate and applies
171/// uniformly to every order constructor.
172#[must_use]
173#[inline]
174fn round_order_size(size: f64) -> f64 {
175    (size * 100.0).round() / 100.0
176}
177
178impl CreateOrderRequest {
179    /// Creates a new market order, typically used for CFD (Contract for Difference) accounts
180    #[must_use]
181    pub fn market(
182        epic: String,
183        direction: Direction,
184        size: f64,
185        currency_code: Option<String>,
186        deal_reference: Option<String>,
187    ) -> Self {
188        let rounded_size = round_order_size(size);
189
190        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
191
192        Self {
193            epic,
194            direction,
195            size: rounded_size,
196            order_type: OrderType::Market,
197            time_in_force: TimeInForce::FillOrKill,
198            level: None,
199            guaranteed_stop: false,
200            stop_level: None,
201            stop_distance: None,
202            limit_level: None,
203            limit_distance: None,
204            expiry: Some("-".to_string()),
205            deal_reference,
206            force_open: true,
207            currency_code,
208            quote_id: None,
209            trailing_stop: Some(false),
210            trailing_stop_increment: None,
211        }
212    }
213
214    /// Creates a new limit order, typically used for CFD (Contract for Difference) accounts
215    #[must_use]
216    pub fn limit(
217        epic: String,
218        direction: Direction,
219        size: f64,
220        level: f64,
221        currency_code: Option<String>,
222        deal_reference: Option<String>,
223    ) -> Self {
224        let rounded_size = round_order_size(size);
225
226        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
227
228        Self {
229            epic,
230            direction,
231            size: rounded_size,
232            order_type: OrderType::Limit,
233            time_in_force: TimeInForce::GoodTillCancelled,
234            level: Some(level),
235            guaranteed_stop: false,
236            stop_level: None,
237            stop_distance: None,
238            limit_level: None,
239            limit_distance: None,
240            expiry: None,
241            deal_reference,
242            force_open: true,
243            currency_code,
244            quote_id: None,
245            trailing_stop: Some(false),
246            trailing_stop_increment: None,
247        }
248    }
249
250    /// Creates a new instance of a market sell option with predefined parameters.
251    ///
252    /// This function sets up a sell option to the market for a given asset (`epic`)
253    /// with the specified size. It configures the order with default values
254    /// for attributes such as direction, order type, and time-in-force.
255    ///
256    /// # Parameters
257    /// - `epic`: A `String` that represents the epic (unique identifier or code) of the instrument
258    ///   being traded.
259    /// - `size`: A `f64` value representing the size or quantity of the order.
260    ///
261    /// # Returns
262    /// An instance of `Self` (the type implementing this function), containing the specified
263    /// `epic` and `size`, along with default values for other parameters:
264    ///
265    /// - `direction`: Set to `Direction::Sell`.
266    /// - `order_type`: Set to `OrderType::Limit`.
267    /// - `time_in_force`: Set to `TimeInForce::FillOrKill`.
268    /// - `level`: Set to `Some(DEFAULT_ORDER_SELL_SIZE)`.
269    /// - `guaranteed_stop`: Set to `false`.
270    /// - `stop_level`: Set to `None`.
271    /// - `stop_distance`: Set to `None`.
272    /// - `limit_level`: Set to `None`.
273    /// - `limit_distance`: Set to `None`.
274    /// - `expiry`: Set based on input or `None`.
275    /// - `deal_reference`: Auto-generated if not provided.
276    /// - `force_open`: Set to `true`.
277    /// - `currency_code`: Defaults to `"EUR"` if not provided.
278    ///
279    /// Note that this function allows for minimal input (the instrument and size),
280    /// while other fields are provided default values. If further customization is required,
281    /// you can modify the returned instance as needed.
282    #[must_use]
283    pub fn sell_option_to_market(
284        epic: String,
285        size: f64,
286        expiry: Option<String>,
287        deal_reference: Option<String>,
288        currency_code: Option<String>,
289    ) -> Self {
290        Self::option_to_market(
291            Direction::Sell,
292            true,
293            epic,
294            size,
295            expiry,
296            deal_reference,
297            currency_code,
298        )
299    }
300
301    /// Shared builder behind the four option-to-market limit-order constructors.
302    ///
303    /// The public `sell_option_to_market` / `buy_option_to_market` variants (with
304    /// and without an explicit `force_open`) differ only by `direction` and
305    /// `force_open`; every other field is identical, so the shared body lives
306    /// here to keep the four wrappers in lock-step. The aggressive limit `level`
307    /// is chosen from `direction` (`DEFAULT_ORDER_SELL_LEVEL` for a sell,
308    /// `DEFAULT_ORDER_BUY_LEVEL` for a buy) so the order fills at market. `size`
309    /// is rounded to two decimals via [`round_order_size`]; a missing
310    /// `deal_reference` is auto-generated, and a missing `currency_code` defaults
311    /// to `"EUR"`.
312    #[must_use]
313    fn option_to_market(
314        direction: Direction,
315        force_open: bool,
316        epic: String,
317        size: f64,
318        expiry: Option<String>,
319        deal_reference: Option<String>,
320        currency_code: Option<String>,
321    ) -> Self {
322        let rounded_size = round_order_size(size);
323
324        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
325
326        let deal_reference = deal_reference.or_else(|| Some(crate::utils::id::get_id()));
327
328        let level = match direction {
329            Direction::Sell => DEFAULT_ORDER_SELL_LEVEL,
330            Direction::Buy => DEFAULT_ORDER_BUY_LEVEL,
331        };
332
333        Self {
334            epic,
335            direction,
336            size: rounded_size,
337            order_type: OrderType::Limit,
338            time_in_force: TimeInForce::FillOrKill,
339            level: Some(level),
340            guaranteed_stop: false,
341            stop_level: None,
342            stop_distance: None,
343            limit_level: None,
344            limit_distance: None,
345            expiry,
346            deal_reference,
347            force_open,
348            currency_code,
349            quote_id: None,
350            trailing_stop: Some(false),
351            trailing_stop_increment: None,
352        }
353    }
354
355    /// Constructs and returns a new instance of the `Self` struct representing a sell option
356    /// to the market with specific parameters for execution.
357    ///
358    /// # Parameters
359    /// - `epic`: A `String` that specifies the EPIC
360    ///   (Exchanged Product Information Code) of the instrument for which the sell order is created.
361    /// - `size`: A `f64` that represents the size of the sell
362    ///   order. The size is rounded to the nearest two decimal places.
363    /// - `expiry`: An optional `String` that indicates the expiry date or period for
364    ///   the sell order. If `None`, no expiry date will be set for the order.
365    /// - `deal_reference`: An optional `String` that contains a reference or identifier
366    ///   for the deal. Can be used for tracking purposes.
367    /// - `currency_code`: An optional `String` representing the currency code. Defaults
368    ///   to `"EUR"` if not provided.
369    /// - `force_open`: A `bool` that specifies whether to force open the
370    ///   position. When `true`, a new position is opened even if an existing position for the
371    ///   same instrument and direction is available.
372    ///
373    /// # Returns
374    /// - `Self`: A new instance populated with the provided parameters, including the following default
375    ///   properties:
376    ///   - `direction`: Set to `Direction::Sell` to designate the sell operation.
377    ///   - `order_type`: Set to `OrderType::Limit` to signify the type of the order.
378    ///   - `time_in_force`: Set to `TimeInForce::FillOrKill` indicating the order should be fully
379    ///     executed or canceled.
380    ///   - `level`: Set to a constant value `DEFAULT_ORDER_SELL_SIZE`.
381    ///   - `guaranteed_stop`: Set to `false`, indicating no guaranteed stop.
382    ///   - Other optional levels/distance fields (`stop_level`, `stop_distance`, `limit_level`,
383    ///     `limit_distance`): Set to `None` by default.
384    ///
385    /// # Notes
386    /// - The input `size` is automatically rounded to the nearest two decimal places before being stored.
387    #[must_use]
388    pub fn sell_option_to_market_w_force(
389        epic: String,
390        size: f64,
391        expiry: Option<String>,
392        deal_reference: Option<String>,
393        currency_code: Option<String>,
394        force_open: bool, // Compensate position if it is already open
395    ) -> Self {
396        Self::option_to_market(
397            Direction::Sell,
398            force_open,
399            epic,
400            size,
401            expiry,
402            deal_reference,
403            currency_code,
404        )
405    }
406
407    /// Creates a new instance of an order to buy an option in the market with specified parameters.
408    ///
409    /// This method initializes an order with the following default values:
410    /// - `direction` is set to `Buy`.
411    /// - `order_type` is set to `Limit`.
412    /// - `time_in_force` is set to `FillOrKill`.
413    /// - `level` is set to `Some(DEFAULT_ORDER_BUY_SIZE)`.
414    /// - `force_open` is set to `true`.
415    ///   Other optional parameters, such as stop levels, distances, expiry, and currency code, are left as `None`.
416    ///
417    /// # Parameters
418    /// - `epic` (`String`): The identifier for the market or instrument to trade.
419    /// - `size` (`f64`): The size or quantity of the order to be executed.
420    ///
421    /// # Returns
422    /// A new instance of `Self` that represents the configured buy option for the given market.
423    ///
424    /// # Note
425    /// Ensure the `epic` and `size` values provided are valid and match required market conditions.
426    #[must_use]
427    pub fn buy_option_to_market(
428        epic: String,
429        size: f64,
430        expiry: Option<String>,
431        deal_reference: Option<String>,
432        currency_code: Option<String>,
433    ) -> Self {
434        Self::option_to_market(
435            Direction::Buy,
436            true,
437            epic,
438            size,
439            expiry,
440            deal_reference,
441            currency_code,
442        )
443    }
444
445    /// Constructs a new instance of an order to buy an option in the market with optional force_open behavior.
446    ///
447    /// # Parameters
448    ///
449    /// * `epic` - A `String` representing the unique identifier of the instrument to be traded.
450    /// * `size` - A `f64` value that represents the size of the order.
451    /// * `expiry` - An optional `String` representing the expiry date of the option.
452    /// * `deal_reference` - An optional `String` for the deal reference identifier.
453    /// * `currency_code` - An optional `String` representing the currency in which the order is denominated.
454    ///   Defaults to "EUR" if not provided.
455    /// * `force_open` - A `bool` indicating whether to force open a new position regardless of existing positions.
456    ///
457    /// # Returns
458    ///
459    /// Returns a new instance of `Self`, representing the constructed order with the provided parameters.
460    ///
461    /// # Behavior
462    ///
463    /// * The size of the order will be rounded to the nearest two decimal places for precision.
464    /// * If a `currency_code` is not provided, the default currency code "EUR" is used.
465    /// * Other parameters are directly mapped into the returned instance.
466    ///
467    /// # Notes
468    ///
469    /// * This function assumes that other order-related fields such as `level`, `stop_level`, `stop_distance`,
470    ///   etc., are set to their defaults or require specific business logic, such as
471    ///   `DEFAULT_ORDER_BUY_SIZE` for the initial buy size.
472    #[must_use]
473    pub fn buy_option_to_market_w_force(
474        epic: String,
475        size: f64,
476        expiry: Option<String>,
477        deal_reference: Option<String>,
478        currency_code: Option<String>,
479        force_open: bool,
480    ) -> Self {
481        Self::option_to_market(
482            Direction::Buy,
483            force_open,
484            epic,
485            size,
486            expiry,
487            deal_reference,
488            currency_code,
489        )
490    }
491
492    /// Adds a stop loss to the order
493    #[must_use]
494    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
495        self.stop_level = Some(stop_level);
496        self
497    }
498
499    /// Adds a take profit to the order
500    #[must_use]
501    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
502        self.limit_level = Some(limit_level);
503        self
504    }
505
506    /// Adds a trailing stop loss to the order
507    #[must_use]
508    pub fn with_trailing_stop_loss(mut self, trailing_stop_increment: f64) -> Self {
509        self.trailing_stop = Some(true);
510        self.trailing_stop_increment = Some(trailing_stop_increment);
511        self
512    }
513
514    /// Adds a reference to the order
515    #[must_use]
516    pub fn with_reference(mut self, reference: String) -> Self {
517        self.deal_reference = Some(reference);
518        self
519    }
520
521    /// Adds a stop distance to the order
522    #[must_use]
523    pub fn with_stop_distance(mut self, stop_distance: f64) -> Self {
524        self.stop_distance = Some(stop_distance);
525        self
526    }
527
528    /// Adds a limit distance to the order
529    #[must_use]
530    pub fn with_limit_distance(mut self, limit_distance: f64) -> Self {
531        self.limit_distance = Some(limit_distance);
532        self
533    }
534
535    /// Adds a guaranteed stop to the order
536    #[must_use]
537    pub fn with_guaranteed_stop(mut self, guaranteed: bool) -> Self {
538        self.guaranteed_stop = guaranteed;
539        self
540    }
541}
542
543/// Model for updating an existing position (PUT /positions/otc/{dealId})
544///
545/// # Constraints
546/// - If `guaranteed_stop` is `true`, then `stop_level` must be set
547/// - If `guaranteed_stop` is `true`, then `trailing_stop` must be `false`
548/// - If `trailing_stop` is `false`, then DO NOT set `trailing_stop_distance` or `trailing_stop_increment`
549/// - If `trailing_stop` is `true`, then `guaranteed_stop` must be `false`
550/// - If `trailing_stop` is `true`, then `trailing_stop_distance`, `trailing_stop_increment`, and `stop_level` must be set
551#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
552pub struct UpdatePositionRequest {
553    /// True if a guaranteed stop is required
554    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
555    pub guaranteed_stop: Option<bool>,
556    /// New price level for take profit
557    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
558    pub limit_level: Option<f64>,
559    /// New price level for stop loss
560    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
561    pub stop_level: Option<f64>,
562    /// True if trailing stop is required
563    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
564    pub trailing_stop: Option<bool>,
565    /// Distance for trailing stop in points
566    #[serde(
567        rename = "trailingStopDistance",
568        skip_serializing_if = "Option::is_none"
569    )]
570    pub trailing_stop_distance: Option<f64>,
571    /// Trailing stop step increment in points
572    #[serde(
573        rename = "trailingStopIncrement",
574        skip_serializing_if = "Option::is_none"
575    )]
576    pub trailing_stop_increment: Option<f64>,
577}
578
579/// Model for closing an existing position
580#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
581pub struct ClosePositionRequest {
582    /// Unique identifier for the position to close
583    #[serde(rename = "dealId", skip_serializing_if = "Option::is_none")]
584    pub deal_id: Option<String>,
585    /// Direction of the closing order (opposite to the position)
586    pub direction: Direction,
587    /// Instrument EPIC identifier
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub epic: Option<String>,
590    /// Expiry date for the order
591    #[serde(rename = "expiry", skip_serializing_if = "Option::is_none")]
592    pub expiry: Option<String>,
593    /// Price level for limit close orders
594    #[serde(rename = "level", skip_serializing_if = "Option::is_none")]
595    pub level: Option<f64>,
596    /// Type of order to use for closing
597    #[serde(rename = "orderType")]
598    pub order_type: OrderType,
599    /// Quote identifier for the order, used for certain order types that require a specific quote
600    #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")]
601    pub quote_id: Option<String>,
602    /// Size/quantity to close
603    pub size: f64,
604    /// Order duration for the closing order
605    #[serde(rename = "timeInForce")]
606    pub time_in_force: TimeInForce,
607}
608
609impl ClosePositionRequest {
610    /// Creates a request to close a position at market price
611    #[must_use]
612    pub fn market(deal_id: String, direction: Direction, size: f64) -> Self {
613        Self {
614            deal_id: Some(deal_id),
615            direction,
616            size,
617            order_type: OrderType::Market,
618            time_in_force: TimeInForce::FillOrKill,
619            level: None,
620            expiry: None,
621            epic: None,
622            quote_id: None,
623        }
624    }
625
626    /// Creates a request to close a position at a specific price level
627    ///
628    /// This is useful for instruments that don't support market orders
629    #[must_use]
630    pub fn limit(deal_id: String, direction: Direction, size: f64, level: f64) -> Self {
631        Self {
632            deal_id: Some(deal_id),
633            direction,
634            size,
635            order_type: OrderType::Limit,
636            time_in_force: TimeInForce::FillOrKill,
637            level: Some(level),
638            expiry: None,
639            epic: None,
640            quote_id: None,
641        }
642    }
643
644    /// Creates a request to close an option position by deal ID using a limit order with predefined price levels
645    ///
646    /// This is specifically designed for options trading where market orders are not supported
647    /// and a limit order with a predefined price level is required based on the direction.
648    ///
649    /// # Arguments
650    /// * `deal_id` - The ID of the deal to close
651    /// * `direction` - The direction of the closing order (opposite of the position direction)
652    /// * `size` - The size of the position to close
653    #[must_use]
654    pub fn close_option_to_market_by_id(deal_id: String, direction: Direction, size: f64) -> Self {
655        // For options, we need to use limit orders with appropriate levels
656        // Use reasonable levels based on direction to ensure fill while being accepted
657        let level = match direction {
658            Direction::Buy => Some(DEFAULT_ORDER_BUY_LEVEL),
659            Direction::Sell => Some(DEFAULT_ORDER_SELL_LEVEL),
660        };
661
662        Self {
663            deal_id: Some(deal_id),
664            direction,
665            size,
666            order_type: OrderType::Limit,
667            time_in_force: TimeInForce::FillOrKill,
668            level,
669            expiry: None,
670            epic: None,
671            quote_id: None,
672        }
673    }
674
675    /// Creates a request to close an option position by epic identifier using a limit order with predefined price levels
676    ///
677    /// This is specifically designed for options trading where market orders are not supported
678    /// and a limit order with a predefined price level is required based on the direction.
679    /// This method is used when the deal ID is not available but the epic and expiry are known.
680    ///
681    /// # Arguments
682    /// * `epic` - The epic identifier of the instrument
683    /// * `expiry` - The expiry date of the option
684    /// * `direction` - The direction of the closing order (opposite of the position direction)
685    /// * `size` - The size of the position to close
686    #[must_use]
687    pub fn close_option_to_market_by_epic(
688        epic: String,
689        expiry: String,
690        direction: Direction,
691        size: f64,
692    ) -> Self {
693        // For options, we need to use limit orders with appropriate levels
694        // Use reasonable levels based on direction to ensure fill while being accepted
695        let level = match direction {
696            Direction::Buy => Some(DEFAULT_ORDER_BUY_LEVEL),
697            Direction::Sell => Some(DEFAULT_ORDER_SELL_LEVEL),
698        };
699
700        Self {
701            deal_id: None,
702            direction,
703            size,
704            order_type: OrderType::Limit,
705            time_in_force: TimeInForce::FillOrKill,
706            level,
707            expiry: Some(expiry),
708            epic: Some(epic),
709            quote_id: None,
710        }
711    }
712}
713
714/// Model for creating a new working order
715#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize)]
716pub struct CreateWorkingOrderRequest {
717    /// Instrument EPIC identifier
718    pub epic: String,
719    /// Order direction (buy or sell)
720    pub direction: Direction,
721    /// Order size/quantity
722    pub size: f64,
723    /// Price level for the order
724    pub level: f64,
725    /// Type of working order (LIMIT or STOP)
726    #[serde(rename = "type")]
727    pub order_type: OrderType,
728    /// Order duration (how long the order remains valid)
729    #[serde(rename = "timeInForce")]
730    pub time_in_force: TimeInForce,
731    /// Whether to use a guaranteed stop
732    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
733    pub guaranteed_stop: Option<bool>,
734    /// Price level for stop loss
735    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
736    pub stop_level: Option<f64>,
737    /// Distance for stop loss
738    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
739    pub stop_distance: Option<f64>,
740    /// Price level for take profit
741    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
742    pub limit_level: Option<f64>,
743    /// Distance for take profit
744    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
745    pub limit_distance: Option<f64>,
746    /// Expiry date for GTD orders
747    #[serde(rename = "goodTillDate", skip_serializing_if = "Option::is_none")]
748    pub good_till_date: Option<String>,
749    /// Client-generated reference for the deal
750    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
751    pub deal_reference: Option<String>,
752    /// Currency code for the order (e.g., "USD", "EUR")
753    #[serde(rename = "currencyCode")]
754    pub currency_code: String,
755    /// Expiry date for the order
756    pub expiry: String,
757}
758
759impl From<WorkingOrder> for CreateWorkingOrderRequest {
760    fn from(value: WorkingOrder) -> Self {
761        let data = value.working_order_data;
762        Self {
763            epic: data.epic,
764            direction: data.direction,
765            size: data.order_size,
766            level: data.order_level,
767            order_type: data.order_type,
768            time_in_force: data.time_in_force,
769            guaranteed_stop: Some(data.guaranteed_stop),
770            stop_level: data.stop_level,
771            stop_distance: data.stop_distance,
772            limit_level: data.limit_level,
773            limit_distance: data.limit_distance,
774            good_till_date: data.good_till_date,
775            deal_reference: data.deal_reference,
776            currency_code: data.currency_code,
777            expiry: value.market_data.expiry,
778        }
779    }
780}
781
782impl CreateWorkingOrderRequest {
783    /// Creates a new limit working order
784    #[must_use]
785    pub fn limit(
786        epic: String,
787        direction: Direction,
788        size: f64,
789        level: f64,
790        currency_code: String,
791        expiry: String,
792    ) -> Self {
793        Self {
794            epic,
795            direction,
796            size,
797            level,
798            order_type: OrderType::Limit,
799            time_in_force: TimeInForce::GoodTillCancelled,
800            guaranteed_stop: Some(false),
801            stop_level: None,
802            stop_distance: None,
803            limit_level: None,
804            limit_distance: None,
805            good_till_date: None,
806            deal_reference: None,
807            currency_code,
808            expiry,
809        }
810    }
811
812    /// Creates a new stop working order
813    #[must_use]
814    pub fn stop(
815        epic: String,
816        direction: Direction,
817        size: f64,
818        level: f64,
819        currency_code: String,
820        expiry: String,
821    ) -> Self {
822        Self {
823            epic,
824            direction,
825            size,
826            level,
827            order_type: OrderType::Stop,
828            time_in_force: TimeInForce::GoodTillCancelled,
829            guaranteed_stop: Some(false),
830            stop_level: None,
831            stop_distance: None,
832            limit_level: None,
833            limit_distance: None,
834            good_till_date: None,
835            deal_reference: None,
836            currency_code,
837            expiry,
838        }
839    }
840
841    /// Adds a stop loss to the working order
842    #[must_use]
843    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
844        self.stop_level = Some(stop_level);
845        self
846    }
847
848    /// Adds a take profit to the working order
849    #[must_use]
850    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
851        self.limit_level = Some(limit_level);
852        self
853    }
854
855    /// Adds a reference to the working order
856    #[must_use]
857    pub fn with_reference(mut self, reference: String) -> Self {
858        self.deal_reference = Some(reference);
859        self
860    }
861
862    /// Sets the expiration date for an order and updates the time-in-force policy.
863    ///
864    /// This method updates the `time_in_force` property to `GoodTillDate` and assigns
865    /// the provided expiration date to the `good_till_date` property. It allows chaining
866    /// as it consumes the current instance and returns it after modification.
867    ///
868    /// # Arguments
869    /// * `date` - A `String` representing the expiration date for the order.
870    ///
871    /// # Returns
872    /// * `Self` - The updated instance of the type, allowing method chaining.
873    ///
874    /// In the example above, the expiration date for the order is set to
875    /// "2023-12-31T23:59:59Z" and the `time_in_force` policy is set to `GoodTillDate`.
876    #[must_use]
877    pub fn expires_at(mut self, date: String) -> Self {
878        self.time_in_force = TimeInForce::GoodTillDate;
879        self.good_till_date = Some(date);
880        self
881    }
882
883    ///
884    /// Sets the order to expire by the end of the next day (tomorrow).
885    ///
886    /// This method modifies the `time_in_force` field to `GoodTillDate` and calculates
887    /// the expiration date as tomorrow's date and time. The calculated date is then
888    /// formatted as a string in the format `YYYY/MM/DD HH:MM:SS` and assigned to
889    /// the `good_till_date` field.
890    ///
891    /// # Returns
892    /// Returns the updated instance of the struct with the expiration date set to tomorrow.
893    ///
894    /// In this example, the `expires_tomorrow` method sets the order to expire at the
895    /// same time on the next calendar day.
896    ///
897    /// Note: The function uses the UTC timezone for calculating the date and time.
898    ///
899    #[must_use]
900    pub fn expires_tomorrow(mut self) -> Self {
901        self.time_in_force = TimeInForce::GoodTillDate;
902        let tomorrow = Utc::now() + Duration::days(1);
903        self.good_till_date = Some(tomorrow.format("%Y/%m/%d %H:%M:%S").to_string());
904        self
905    }
906
907    /// Sets the expiration time for an order and configures it to expire at a specific date and time.
908    ///
909    /// This method modifies the `time_in_force` for the order to `GoodTillDate`
910    /// and calculates the expiration time by adding the provided `duration` to
911    /// the current UTC time. The calculated expiration time is formatted as
912    /// "YYYY/MM/DD HH:MM:SS" and stored in the `good_till_date` field.
913    ///
914    /// # Parameters
915    /// - `duration`: A `Duration` instance that represents the amount of time
916    ///   after the current UTC time when the order should expire.
917    ///
918    /// # Returns
919    /// Returns `Self` with updated `time_in_force` and `good_till_date`.
920    ///
921    /// Note: This method assumes that the runtime uses the `chrono` crate for
922    /// time handling and formatting.
923    #[must_use]
924    pub fn expires_in(mut self, duration: Duration) -> Self {
925        self.time_in_force = TimeInForce::GoodTillDate;
926        let tomorrow = Utc::now() + duration;
927        self.good_till_date = Some(tomorrow.format("%Y/%m/%d %H:%M:%S").to_string());
928        self
929    }
930}
931
932// ============================================================================
933// WATCHLIST REQUESTS
934// ============================================================================
935
936/// Request to create a new watchlist
937#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
938pub struct CreateWatchlistRequest {
939    /// Name for the new watchlist
940    pub name: String,
941    /// Optional list of EPICs to add to the watchlist
942    #[serde(skip_serializing_if = "Option::is_none")]
943    pub epics: Option<Vec<String>>,
944}
945
946impl CreateWatchlistRequest {
947    /// Create a new watchlist request with just a name
948    #[must_use]
949    pub fn new(name: &str) -> Self {
950        Self {
951            name: name.to_string(),
952            epics: None,
953        }
954    }
955
956    /// Create a watchlist request with name and epics
957    #[must_use]
958    pub fn with_epics(name: &str, epics: Vec<String>) -> Self {
959        Self {
960            name: name.to_string(),
961            epics: Some(epics),
962        }
963    }
964}
965
966/// Request to add an instrument to a watchlist
967#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
968pub struct AddToWatchlistRequest {
969    /// EPIC of the instrument to add
970    pub epic: String,
971}
972
973// ============================================================================
974// WORKING ORDER UPDATE REQUEST
975// ============================================================================
976
977/// Request to update an existing working order
978#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
979pub struct UpdateWorkingOrderRequest {
980    /// Good till date for the order (format: yyyy/MM/dd HH:mm:ss)
981    #[serde(rename = "goodTillDate", skip_serializing_if = "Option::is_none")]
982    pub good_till_date: Option<String>,
983    /// Order level
984    pub level: f64,
985    /// Distance from current price to limit level
986    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
987    pub limit_distance: Option<f64>,
988    /// Limit level
989    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
990    pub limit_level: Option<f64>,
991    /// Distance from current price to stop level
992    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
993    pub stop_distance: Option<f64>,
994    /// Stop level
995    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
996    pub stop_level: Option<f64>,
997    /// Whether the stop is guaranteed
998    #[serde(rename = "guaranteedStop")]
999    pub guaranteed_stop: bool,
1000    /// Time in force
1001    #[serde(rename = "timeInForce")]
1002    pub time_in_force: TimeInForce,
1003    /// Order type (LIMIT or STOP)
1004    #[serde(rename = "type")]
1005    pub order_type: OrderType,
1006}
1007
1008impl UpdateWorkingOrderRequest {
1009    /// Create a new update working order request
1010    #[must_use]
1011    pub fn new(level: f64, order_type: OrderType, time_in_force: TimeInForce) -> Self {
1012        Self {
1013            level,
1014            order_type,
1015            time_in_force,
1016            guaranteed_stop: false,
1017            good_till_date: None,
1018            limit_distance: None,
1019            limit_level: None,
1020            stop_distance: None,
1021            stop_level: None,
1022        }
1023    }
1024
1025    /// Set the stop level
1026    #[must_use]
1027    pub fn with_stop_level(mut self, stop_level: f64) -> Self {
1028        self.stop_level = Some(stop_level);
1029        self
1030    }
1031
1032    /// Set the limit level
1033    #[must_use]
1034    pub fn with_limit_level(mut self, limit_level: f64) -> Self {
1035        self.limit_level = Some(limit_level);
1036        self
1037    }
1038
1039    /// Set guaranteed stop
1040    #[must_use]
1041    pub fn with_guaranteed_stop(mut self, guaranteed: bool) -> Self {
1042        self.guaranteed_stop = guaranteed;
1043        self
1044    }
1045}
1046
1047// ============================================================================
1048// INDICATIVE COSTS REQUESTS
1049// ============================================================================
1050
1051/// Request for indicative costs when opening a position
1052#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
1053pub struct OpenCostsRequest {
1054    /// Instrument epic
1055    pub epic: String,
1056    /// Trade direction (BUY or SELL)
1057    pub direction: Direction,
1058    /// Trade size
1059    pub size: f64,
1060    /// Order type
1061    #[serde(rename = "orderType")]
1062    pub order_type: OrderType,
1063    /// Currency code
1064    #[serde(rename = "currencyCode")]
1065    pub currency_code: String,
1066    /// Whether the stop is guaranteed
1067    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
1068    pub guaranteed_stop: Option<bool>,
1069    /// Stop distance
1070    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
1071    pub stop_distance: Option<f64>,
1072    /// Limit distance
1073    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
1074    pub limit_distance: Option<f64>,
1075}
1076
1077impl OpenCostsRequest {
1078    /// Create a new open costs request
1079    #[must_use]
1080    pub fn new(epic: &str, direction: Direction, size: f64, currency_code: &str) -> Self {
1081        Self {
1082            epic: epic.to_string(),
1083            direction,
1084            size,
1085            order_type: OrderType::Market,
1086            currency_code: currency_code.to_string(),
1087            guaranteed_stop: None,
1088            stop_distance: None,
1089            limit_distance: None,
1090        }
1091    }
1092}
1093
1094/// Request for indicative costs when closing a position
1095#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
1096pub struct CloseCostsRequest {
1097    /// Deal ID of the position to close
1098    #[serde(rename = "dealId")]
1099    pub deal_id: String,
1100    /// Trade direction (opposite of the position direction)
1101    pub direction: Direction,
1102    /// Size to close
1103    pub size: f64,
1104    /// Order type
1105    #[serde(rename = "orderType")]
1106    pub order_type: OrderType,
1107}
1108
1109impl CloseCostsRequest {
1110    /// Create a new close costs request
1111    #[must_use]
1112    pub fn new(deal_id: &str, direction: Direction, size: f64) -> Self {
1113        Self {
1114            deal_id: deal_id.to_string(),
1115            direction,
1116            size,
1117            order_type: OrderType::Market,
1118        }
1119    }
1120}
1121
1122/// Request for indicative costs when editing a position
1123#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
1124pub struct EditCostsRequest {
1125    /// Deal ID of the position to edit
1126    #[serde(rename = "dealId")]
1127    pub deal_id: String,
1128    /// New stop level
1129    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
1130    pub stop_level: Option<f64>,
1131    /// New limit level
1132    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
1133    pub limit_level: Option<f64>,
1134    /// Whether the stop is guaranteed
1135    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
1136    pub guaranteed_stop: Option<bool>,
1137}
1138
1139impl EditCostsRequest {
1140    /// Create a new edit costs request
1141    #[must_use]
1142    pub fn new(deal_id: &str) -> Self {
1143        Self {
1144            deal_id: deal_id.to_string(),
1145            stop_level: None,
1146            limit_level: None,
1147            guaranteed_stop: None,
1148        }
1149    }
1150
1151    /// Set the new stop level
1152    #[must_use]
1153    pub fn with_stop_level(mut self, stop_level: f64) -> Self {
1154        self.stop_level = Some(stop_level);
1155        self
1156    }
1157
1158    /// Set the new limit level
1159    #[must_use]
1160    pub fn with_limit_level(mut self, limit_level: f64) -> Self {
1161        self.limit_level = Some(limit_level);
1162        self
1163    }
1164}
1165
1166// ============================================================================
1167// ACCOUNT PREFERENCES REQUEST
1168// ============================================================================
1169
1170/// Request to update account preferences
1171#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
1172pub struct UpdatePreferencesRequest {
1173    /// Whether trailing stops should be enabled
1174    #[serde(rename = "trailingStopsEnabled")]
1175    pub trailing_stops_enabled: bool,
1176}