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::prelude::{Deserialize, Serialize, WorkingOrder};
8use crate::presentation::order::{Direction, OrderType, TimeInForce};
9use chrono::{Duration, Utc};
10use pretty_simple_display::{DebugPretty, DisplaySimple};
11use std::fmt;
12use std::fmt::{Debug, Display};
13
14/// Parameters for getting recent prices (API v3)
15#[derive(Clone, Default, Deserialize, Serialize)]
16pub struct RecentPricesRequest<'a> {
17    /// Instrument epic
18    pub epic: &'a str,
19    /// Optional price resolution (default: MINUTE)
20    pub resolution: Option<&'a str>,
21    /// Optional start date time (yyyy-MM-dd'T'HH:mm:ss)
22    pub from: Option<&'a str>,
23    /// Optional end date time (yyyy-MM-dd'T'HH:mm:ss)
24    pub to: Option<&'a str>,
25    /// Optional max number of price points (default: 10)
26    pub max_points: Option<i32>,
27    /// Optional page size (default: 20, disable paging = 0)
28    pub page_size: Option<i32>,
29    /// Optional page number (default: 1)
30    pub page_number: Option<i32>,
31}
32
33impl<'a> RecentPricesRequest<'a> {
34    /// Create new parameters with just the epic (required field)
35    #[must_use]
36    pub fn new(epic: &'a str) -> Self {
37        Self {
38            epic,
39            ..Default::default()
40        }
41    }
42
43    /// Set the resolution
44    #[must_use]
45    pub fn with_resolution(mut self, resolution: &'a str) -> Self {
46        self.resolution = Some(resolution);
47        self
48    }
49
50    /// Set the from date
51    #[must_use]
52    pub fn with_from(mut self, from: &'a str) -> Self {
53        self.from = Some(from);
54        self
55    }
56
57    /// Set the to date
58    #[must_use]
59    pub fn with_to(mut self, to: &'a str) -> Self {
60        self.to = Some(to);
61        self
62    }
63
64    /// Set the max points
65    #[must_use]
66    pub fn with_max_points(mut self, max_points: i32) -> Self {
67        self.max_points = Some(max_points);
68        self
69    }
70
71    /// Set the page size
72    #[must_use]
73    pub fn with_page_size(mut self, page_size: i32) -> Self {
74        self.page_size = Some(page_size);
75        self
76    }
77
78    /// Set the page number
79    #[must_use]
80    pub fn with_page_number(mut self, page_number: i32) -> Self {
81        self.page_number = Some(page_number);
82        self
83    }
84}
85
86impl Display for RecentPricesRequest<'_> {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        let json = serde_json::to_string(self).unwrap_or_else(|_| "Invalid JSON".to_string());
89        write!(f, "{}", json)
90    }
91}
92
93impl Debug for RecentPricesRequest<'_> {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        let json =
96            serde_json::to_string_pretty(self).unwrap_or_else(|_| "Invalid JSON".to_string());
97        write!(f, "{}", json)
98    }
99}
100
101/// Model for creating a new order
102#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
103pub struct CreateOrderRequest {
104    /// Instrument EPIC identifier
105    pub epic: String,
106    /// Order direction (buy or sell)
107    pub direction: Direction,
108    /// Order size/quantity
109    pub size: f64,
110    /// Type of order (market, limit, etc.)
111    #[serde(rename = "orderType")]
112    pub order_type: OrderType,
113    /// Order duration (how long the order remains valid)
114    #[serde(rename = "timeInForce")]
115    pub time_in_force: TimeInForce,
116    /// Price level for limit orders
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub level: Option<f64>,
119    /// Whether to use a guaranteed stop
120    #[serde(rename = "guaranteedStop")]
121    pub guaranteed_stop: bool,
122    /// Price level for stop loss
123    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
124    pub stop_level: Option<f64>,
125    /// Stop loss distance
126    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
127    pub stop_distance: Option<f64>,
128    /// Price level for take profit
129    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
130    pub limit_level: Option<f64>,
131    /// Take profit distance
132    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
133    pub limit_distance: Option<f64>,
134    /// Expiry date for the order
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub expiry: Option<String>,
137    /// Client-generated reference for the deal
138    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
139    pub deal_reference: Option<String>,
140    /// Whether to force open a new position
141    #[serde(rename = "forceOpen")]
142    pub force_open: bool,
143    /// Currency code for the order (e.g., "USD", "EUR")
144    #[serde(rename = "currencyCode")]
145    pub currency_code: String,
146    /// Quote identifier for the order
147    #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")]
148    pub quote_id: Option<String>,
149    /// Trailing stop enabled
150    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
151    pub trailing_stop: Option<bool>,
152    /// Trailing stop increment (only if trailingStop is true)
153    #[serde(
154        rename = "trailingStopIncrement",
155        skip_serializing_if = "Option::is_none"
156    )]
157    pub trailing_stop_increment: Option<f64>,
158}
159
160impl CreateOrderRequest {
161    /// Creates a new market order, typically used for CFD (Contract for Difference) accounts
162    #[must_use]
163    pub fn market(
164        epic: String,
165        direction: Direction,
166        size: f64,
167        currency_code: Option<String>,
168        deal_reference: Option<String>,
169    ) -> Self {
170        let rounded_size = (size * 100.0).floor() / 100.0;
171
172        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
173
174        Self {
175            epic,
176            direction,
177            size: rounded_size,
178            order_type: OrderType::Market,
179            time_in_force: TimeInForce::FillOrKill,
180            level: None,
181            guaranteed_stop: false,
182            stop_level: None,
183            stop_distance: None,
184            limit_level: None,
185            limit_distance: None,
186            expiry: Some("-".to_string()),
187            deal_reference,
188            force_open: true,
189            currency_code,
190            quote_id: None,
191            trailing_stop: Some(false),
192            trailing_stop_increment: None,
193        }
194    }
195
196    /// Creates a new limit order, typically used for CFD (Contract for Difference) accounts
197    #[must_use]
198    pub fn limit(
199        epic: String,
200        direction: Direction,
201        size: f64,
202        level: f64,
203        currency_code: Option<String>,
204        deal_reference: Option<String>,
205    ) -> Self {
206        let rounded_size = (size * 100.0).floor() / 100.0;
207
208        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
209
210        Self {
211            epic,
212            direction,
213            size: rounded_size,
214            order_type: OrderType::Limit,
215            time_in_force: TimeInForce::GoodTillCancelled,
216            level: Some(level),
217            guaranteed_stop: false,
218            stop_level: None,
219            stop_distance: None,
220            limit_level: None,
221            limit_distance: None,
222            expiry: None,
223            deal_reference,
224            force_open: true,
225            currency_code,
226            quote_id: None,
227            trailing_stop: Some(false),
228            trailing_stop_increment: None,
229        }
230    }
231
232    /// Creates a new instance of a market sell option with predefined parameters.
233    ///
234    /// This function sets up a sell option to the market for a given asset (`epic`)
235    /// with the specified size. It configures the order with default values
236    /// for attributes such as direction, order type, and time-in-force.
237    ///
238    /// # Parameters
239    /// - `epic`: A `String` that represents the epic (unique identifier or code) of the instrument
240    ///   being traded.
241    /// - `size`: A `f64` value representing the size or quantity of the order.
242    ///
243    /// # Returns
244    /// An instance of `Self` (the type implementing this function), containing the specified
245    /// `epic` and `size`, along with default values for other parameters:
246    ///
247    /// - `direction`: Set to `Direction::Sell`.
248    /// - `order_type`: Set to `OrderType::Limit`.
249    /// - `time_in_force`: Set to `TimeInForce::FillOrKill`.
250    /// - `level`: Set to `Some(DEFAULT_ORDER_SELL_SIZE)`.
251    /// - `guaranteed_stop`: Set to `false`.
252    /// - `stop_level`: Set to `None`.
253    /// - `stop_distance`: Set to `None`.
254    /// - `limit_level`: Set to `None`.
255    /// - `limit_distance`: Set to `None`.
256    /// - `expiry`: Set based on input or `None`.
257    /// - `deal_reference`: Auto-generated if not provided.
258    /// - `force_open`: Set to `true`.
259    /// - `currency_code`: Defaults to `"EUR"` if not provided.
260    ///
261    /// Note that this function allows for minimal input (the instrument and size),
262    /// while other fields are provided default values. If further customization is required,
263    /// you can modify the returned instance as needed.
264    #[must_use]
265    pub fn sell_option_to_market(
266        epic: String,
267        size: f64,
268        expiry: Option<String>,
269        deal_reference: Option<String>,
270        currency_code: Option<String>,
271    ) -> Self {
272        let rounded_size = (size * 100.0).floor() / 100.0;
273
274        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
275
276        let deal_reference =
277            deal_reference.or_else(|| Some(nanoid::nanoid!(30, &nanoid::alphabet::SAFE)));
278
279        Self {
280            epic,
281            direction: Direction::Sell,
282            size: rounded_size,
283            order_type: OrderType::Limit,
284            time_in_force: TimeInForce::FillOrKill,
285            level: Some(DEFAULT_ORDER_SELL_LEVEL),
286            guaranteed_stop: false,
287            stop_level: None,
288            stop_distance: None,
289            limit_level: None,
290            limit_distance: None,
291            expiry: expiry.clone(),
292            deal_reference: deal_reference.clone(),
293            force_open: true,
294            currency_code,
295            quote_id: None,
296            trailing_stop: Some(false),
297            trailing_stop_increment: None,
298        }
299    }
300
301    /// Constructs and returns a new instance of the `Self` struct representing a sell option
302    /// to the market with specific parameters for execution.
303    ///
304    /// # Parameters
305    /// - `epic`: A `String` that specifies the EPIC
306    ///   (Exchanged Product Information Code) of the instrument for which the sell order is created.
307    /// - `size`: A `f64` that represents the size of the sell
308    ///   order. The size is rounded to two decimal places.
309    /// - `expiry`: An optional `String` that indicates the expiry date or period for
310    ///   the sell order. If `None`, no expiry date will be set for the order.
311    /// - `deal_reference`: An optional `String` that contains a reference or identifier
312    ///   for the deal. Can be used for tracking purposes.
313    /// - `currency_code`: An optional `String` representing the currency code. Defaults
314    ///   to `"EUR"` if not provided.
315    /// - `force_open`: A `bool` that specifies whether to force open the
316    ///   position. When `true`, a new position is opened even if an existing position for the
317    ///   same instrument and direction is available.
318    ///
319    /// # Returns
320    /// - `Self`: A new instance populated with the provided parameters, including the following default
321    ///   properties:
322    ///   - `direction`: Set to `Direction::Sell` to designate the sell operation.
323    ///   - `order_type`: Set to `OrderType::Limit` to signify the type of the order.
324    ///   - `time_in_force`: Set to `TimeInForce::FillOrKill` indicating the order should be fully
325    ///     executed or canceled.
326    ///   - `level`: Set to a constant value `DEFAULT_ORDER_SELL_SIZE`.
327    ///   - `guaranteed_stop`: Set to `false`, indicating no guaranteed stop.
328    ///   - Other optional levels/distance fields (`stop_level`, `stop_distance`, `limit_level`,
329    ///     `limit_distance`): Set to `None` by default.
330    ///
331    /// # Notes
332    /// - The input `size` is automatically rounded down to two decimal places before being stored.
333    #[must_use]
334    pub fn sell_option_to_market_w_force(
335        epic: String,
336        size: f64,
337        expiry: Option<String>,
338        deal_reference: Option<String>,
339        currency_code: Option<String>,
340        force_open: bool, // Compensate position if it is already open
341    ) -> Self {
342        let rounded_size = (size * 100.0).floor() / 100.0;
343
344        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
345
346        let deal_reference =
347            deal_reference.or_else(|| Some(nanoid::nanoid!(30, &nanoid::alphabet::SAFE)));
348
349        Self {
350            epic,
351            direction: Direction::Sell,
352            size: rounded_size,
353            order_type: OrderType::Limit,
354            time_in_force: TimeInForce::FillOrKill,
355            level: Some(DEFAULT_ORDER_SELL_LEVEL),
356            guaranteed_stop: false,
357            stop_level: None,
358            stop_distance: None,
359            limit_level: None,
360            limit_distance: None,
361            expiry: expiry.clone(),
362            deal_reference: deal_reference.clone(),
363            force_open,
364            currency_code,
365            quote_id: None,
366            trailing_stop: Some(false),
367            trailing_stop_increment: None,
368        }
369    }
370
371    /// Creates a new instance of an order to buy an option in the market with specified parameters.
372    ///
373    /// This method initializes an order with the following default values:
374    /// - `direction` is set to `Buy`.
375    /// - `order_type` is set to `Limit`.
376    /// - `time_in_force` is set to `FillOrKill`.
377    /// - `level` is set to `Some(DEFAULT_ORDER_BUY_SIZE)`.
378    /// - `force_open` is set to `true`.
379    ///   Other optional parameters, such as stop levels, distances, expiry, and currency code, are left as `None`.
380    ///
381    /// # Parameters
382    /// - `epic` (`String`): The identifier for the market or instrument to trade.
383    /// - `size` (`f64`): The size or quantity of the order to be executed.
384    ///
385    /// # Returns
386    /// A new instance of `Self` that represents the configured buy option for the given market.
387    ///
388    /// # Note
389    /// Ensure the `epic` and `size` values provided are valid and match required market conditions.
390    #[must_use]
391    pub fn buy_option_to_market(
392        epic: String,
393        size: f64,
394        expiry: Option<String>,
395        deal_reference: Option<String>,
396        currency_code: Option<String>,
397    ) -> Self {
398        let rounded_size = (size * 100.0).floor() / 100.0;
399
400        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
401
402        let deal_reference =
403            deal_reference.or_else(|| Some(nanoid::nanoid!(30, &nanoid::alphabet::SAFE)));
404
405        Self {
406            epic,
407            direction: Direction::Buy,
408            size: rounded_size,
409            order_type: OrderType::Limit,
410            time_in_force: TimeInForce::FillOrKill,
411            level: Some(DEFAULT_ORDER_BUY_LEVEL),
412            guaranteed_stop: false,
413            stop_level: None,
414            stop_distance: None,
415            limit_level: None,
416            limit_distance: None,
417            expiry: expiry.clone(),
418            deal_reference: deal_reference.clone(),
419            force_open: true,
420            currency_code: currency_code.clone(),
421            quote_id: None,
422            trailing_stop: Some(false),
423            trailing_stop_increment: None,
424        }
425    }
426
427    /// Constructs a new instance of an order to buy an option in the market with optional force_open behavior.
428    ///
429    /// # Parameters
430    ///
431    /// * `epic` - A `String` representing the unique identifier of the instrument to be traded.
432    /// * `size` - A `f64` value that represents the size of the order.
433    /// * `expiry` - An optional `String` representing the expiry date of the option.
434    /// * `deal_reference` - An optional `String` for the deal reference identifier.
435    /// * `currency_code` - An optional `String` representing the currency in which the order is denominated.
436    ///   Defaults to "EUR" if not provided.
437    /// * `force_open` - A `bool` indicating whether to force open a new position regardless of existing positions.
438    ///
439    /// # Returns
440    ///
441    /// Returns a new instance of `Self`, representing the constructed order with the provided parameters.
442    ///
443    /// # Behavior
444    ///
445    /// * The size of the order will be rounded down to two decimal places for precision.
446    /// * If a `currency_code` is not provided, the default currency code "EUR" is used.
447    /// * Other parameters are directly mapped into the returned instance.
448    ///
449    /// # Notes
450    ///
451    /// * This function assumes that other order-related fields such as `level`, `stop_level`, `stop_distance`,
452    ///   etc., are set to their defaults or require specific business logic, such as
453    ///   `DEFAULT_ORDER_BUY_SIZE` for the initial buy size.
454    #[must_use]
455    pub fn buy_option_to_market_w_force(
456        epic: String,
457        size: f64,
458        expiry: Option<String>,
459        deal_reference: Option<String>,
460        currency_code: Option<String>,
461        force_open: bool,
462    ) -> Self {
463        let rounded_size = (size * 100.0).floor() / 100.0;
464
465        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
466
467        let deal_reference =
468            deal_reference.or_else(|| Some(nanoid::nanoid!(30, &nanoid::alphabet::SAFE)));
469
470        Self {
471            epic,
472            direction: Direction::Buy,
473            size: rounded_size,
474            order_type: OrderType::Limit,
475            time_in_force: TimeInForce::FillOrKill,
476            level: Some(DEFAULT_ORDER_BUY_LEVEL),
477            guaranteed_stop: false,
478            stop_level: None,
479            stop_distance: None,
480            limit_level: None,
481            limit_distance: None,
482            expiry: expiry.clone(),
483            deal_reference: deal_reference.clone(),
484            force_open,
485            currency_code: currency_code.clone(),
486            quote_id: None,
487            trailing_stop: Some(false),
488            trailing_stop_increment: None,
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, Default)]
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, Default)]
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, Default)]
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, Default)]
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, Default)]
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}