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    pub fn new(epic: &'a str) -> Self {
36        Self {
37            epic,
38            ..Default::default()
39        }
40    }
41
42    /// Set the resolution
43    pub fn with_resolution(mut self, resolution: &'a str) -> Self {
44        self.resolution = Some(resolution);
45        self
46    }
47
48    /// Set the from date
49    pub fn with_from(mut self, from: &'a str) -> Self {
50        self.from = Some(from);
51        self
52    }
53
54    /// Set the to date
55    pub fn with_to(mut self, to: &'a str) -> Self {
56        self.to = Some(to);
57        self
58    }
59
60    /// Set the max points
61    pub fn with_max_points(mut self, max_points: i32) -> Self {
62        self.max_points = Some(max_points);
63        self
64    }
65
66    /// Set the page size
67    pub fn with_page_size(mut self, page_size: i32) -> Self {
68        self.page_size = Some(page_size);
69        self
70    }
71
72    /// Set the page number
73    pub fn with_page_number(mut self, page_number: i32) -> Self {
74        self.page_number = Some(page_number);
75        self
76    }
77}
78
79impl Display for RecentPricesRequest<'_> {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        let json = serde_json::to_string(self).unwrap_or_else(|_| "Invalid JSON".to_string());
82        write!(f, "{}", json)
83    }
84}
85
86impl Debug for RecentPricesRequest<'_> {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        let json =
89            serde_json::to_string_pretty(self).unwrap_or_else(|_| "Invalid JSON".to_string());
90        write!(f, "{}", json)
91    }
92}
93
94/// Model for creating a new order
95#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
96pub struct CreateOrderRequest {
97    /// Instrument EPIC identifier
98    pub epic: String,
99    /// Order direction (buy or sell)
100    pub direction: Direction,
101    /// Order size/quantity
102    pub size: f64,
103    /// Type of order (market, limit, etc.)
104    #[serde(rename = "orderType")]
105    pub order_type: OrderType,
106    /// Order duration (how long the order remains valid)
107    #[serde(rename = "timeInForce")]
108    pub time_in_force: TimeInForce,
109    /// Price level for limit orders
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub level: Option<f64>,
112    /// Whether to use a guaranteed stop
113    #[serde(rename = "guaranteedStop")]
114    pub guaranteed_stop: bool,
115    /// Price level for stop loss
116    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
117    pub stop_level: Option<f64>,
118    /// Stop loss distance
119    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
120    pub stop_distance: Option<f64>,
121    /// Price level for take profit
122    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
123    pub limit_level: Option<f64>,
124    /// Take profit distance
125    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
126    pub limit_distance: Option<f64>,
127    /// Expiry date for the order
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub expiry: Option<String>,
130    /// Client-generated reference for the deal
131    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
132    pub deal_reference: Option<String>,
133    /// Whether to force open a new position
134    #[serde(rename = "forceOpen")]
135    pub force_open: bool,
136    /// Currency code for the order (e.g., "USD", "EUR")
137    #[serde(rename = "currencyCode")]
138    pub currency_code: String,
139    /// Quote identifier for the order
140    #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")]
141    pub quote_id: Option<String>,
142    /// Trailing stop enabled
143    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
144    pub trailing_stop: Option<bool>,
145    /// Trailing stop increment (only if trailingStop is true)
146    #[serde(
147        rename = "trailingStopIncrement",
148        skip_serializing_if = "Option::is_none"
149    )]
150    pub trailing_stop_increment: Option<f64>,
151}
152
153impl CreateOrderRequest {
154    /// Creates a new market order, typically used for CFD (Contract for Difference) accounts
155    pub fn market(
156        epic: String,
157        direction: Direction,
158        size: f64,
159        currency_code: Option<String>,
160        deal_reference: Option<String>,
161    ) -> Self {
162        let rounded_size = (size * 100.0).floor() / 100.0;
163
164        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
165
166        Self {
167            epic,
168            direction,
169            size: rounded_size,
170            order_type: OrderType::Market,
171            time_in_force: TimeInForce::FillOrKill,
172            level: None,
173            guaranteed_stop: false,
174            stop_level: None,
175            stop_distance: None,
176            limit_level: None,
177            limit_distance: None,
178            expiry: Some("-".to_string()),
179            deal_reference,
180            force_open: true,
181            currency_code,
182            quote_id: None,
183            trailing_stop: Some(false),
184            trailing_stop_increment: None,
185        }
186    }
187
188    /// Creates a new limit order, typically used for CFD (Contract for Difference) accounts
189    pub fn limit(
190        epic: String,
191        direction: Direction,
192        size: f64,
193        level: f64,
194        currency_code: Option<String>,
195        deal_reference: Option<String>,
196    ) -> Self {
197        let rounded_size = (size * 100.0).floor() / 100.0;
198
199        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
200
201        Self {
202            epic,
203            direction,
204            size: rounded_size,
205            order_type: OrderType::Limit,
206            time_in_force: TimeInForce::GoodTillCancelled,
207            level: Some(level),
208            guaranteed_stop: false,
209            stop_level: None,
210            stop_distance: None,
211            limit_level: None,
212            limit_distance: None,
213            expiry: None,
214            deal_reference,
215            force_open: true,
216            currency_code,
217            quote_id: None,
218            trailing_stop: Some(false),
219            trailing_stop_increment: None,
220        }
221    }
222
223    /// Creates a new instance of a market sell option with predefined parameters.
224    ///
225    /// This function sets up a sell option to the market for a given asset (`epic`)
226    /// with the specified size. It configures the order with default values
227    /// for attributes such as direction, order type, and time-in-force.
228    ///
229    /// # Parameters
230    /// - `epic`: A `String` that represents the epic (unique identifier or code) of the instrument
231    ///   being traded.
232    /// - `size`: A `f64` value representing the size or quantity of the order.
233    ///
234    /// # Returns
235    /// An instance of `Self` (the type implementing this function), containing the specified
236    /// `epic` and `size`, along with default values for other parameters:
237    ///
238    /// - `direction`: Set to `Direction::Sell`.
239    /// - `order_type`: Set to `OrderType::Limit`.
240    /// - `time_in_force`: Set to `TimeInForce::FillOrKill`.
241    /// - `level`: Set to `Some(DEFAULT_ORDER_SELL_SIZE)`.
242    /// - `guaranteed_stop`: Set to `false`.
243    /// - `stop_level`: Set to `None`.
244    /// - `stop_distance`: Set to `None`.
245    /// - `limit_level`: Set to `None`.
246    /// - `limit_distance`: Set to `None`.
247    /// - `expiry`: Set based on input or `None`.
248    /// - `deal_reference`: Auto-generated if not provided.
249    /// - `force_open`: Set to `true`.
250    /// - `currency_code`: Defaults to `"EUR"` if not provided.
251    ///
252    /// Note that this function allows for minimal input (the instrument and size),
253    /// while other fields are provided default values. If further customization is required,
254    /// you can modify the returned instance as needed.
255    pub fn sell_option_to_market(
256        epic: String,
257        size: f64,
258        expiry: Option<String>,
259        deal_reference: Option<String>,
260        currency_code: Option<String>,
261    ) -> Self {
262        let rounded_size = (size * 100.0).floor() / 100.0;
263
264        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
265
266        let deal_reference =
267            deal_reference.or_else(|| Some(nanoid::nanoid!(30, &nanoid::alphabet::SAFE)));
268
269        Self {
270            epic,
271            direction: Direction::Sell,
272            size: rounded_size,
273            order_type: OrderType::Limit,
274            time_in_force: TimeInForce::FillOrKill,
275            level: Some(DEFAULT_ORDER_SELL_LEVEL),
276            guaranteed_stop: false,
277            stop_level: None,
278            stop_distance: None,
279            limit_level: None,
280            limit_distance: None,
281            expiry: expiry.clone(),
282            deal_reference: deal_reference.clone(),
283            force_open: true,
284            currency_code,
285            quote_id: None,
286            trailing_stop: Some(false),
287            trailing_stop_increment: None,
288        }
289    }
290
291    /// Constructs and returns a new instance of the `Self` struct representing a sell option
292    /// to the market with specific parameters for execution.
293    ///
294    /// # Parameters
295    /// - `epic`: A `String` that specifies the EPIC
296    ///   (Exchanged Product Information Code) of the instrument for which the sell order is created.
297    /// - `size`: A `f64` that represents the size of the sell
298    ///   order. The size is rounded to two decimal places.
299    /// - `expiry`: An optional `String` that indicates the expiry date or period for
300    ///   the sell order. If `None`, no expiry date will be set for the order.
301    /// - `deal_reference`: An optional `String` that contains a reference or identifier
302    ///   for the deal. Can be used for tracking purposes.
303    /// - `currency_code`: An optional `String` representing the currency code. Defaults
304    ///   to `"EUR"` if not provided.
305    /// - `force_open`: A `bool` that specifies whether to force open the
306    ///   position. When `true`, a new position is opened even if an existing position for the
307    ///   same instrument and direction is available.
308    ///
309    /// # Returns
310    /// - `Self`: A new instance populated with the provided parameters, including the following default
311    ///   properties:
312    ///   - `direction`: Set to `Direction::Sell` to designate the sell operation.
313    ///   - `order_type`: Set to `OrderType::Limit` to signify the type of the order.
314    ///   - `time_in_force`: Set to `TimeInForce::FillOrKill` indicating the order should be fully
315    ///     executed or canceled.
316    ///   - `level`: Set to a constant value `DEFAULT_ORDER_SELL_SIZE`.
317    ///   - `guaranteed_stop`: Set to `false`, indicating no guaranteed stop.
318    ///   - Other optional levels/distance fields (`stop_level`, `stop_distance`, `limit_level`,
319    ///     `limit_distance`): Set to `None` by default.
320    ///
321    /// # Notes
322    /// - The input `size` is automatically rounded down to two decimal places before being stored.
323    pub fn sell_option_to_market_w_force(
324        epic: String,
325        size: f64,
326        expiry: Option<String>,
327        deal_reference: Option<String>,
328        currency_code: Option<String>,
329        force_open: bool, // Compensate position if it is already open
330    ) -> Self {
331        let rounded_size = (size * 100.0).floor() / 100.0;
332
333        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
334
335        let deal_reference =
336            deal_reference.or_else(|| Some(nanoid::nanoid!(30, &nanoid::alphabet::SAFE)));
337
338        Self {
339            epic,
340            direction: Direction::Sell,
341            size: rounded_size,
342            order_type: OrderType::Limit,
343            time_in_force: TimeInForce::FillOrKill,
344            level: Some(DEFAULT_ORDER_SELL_LEVEL),
345            guaranteed_stop: false,
346            stop_level: None,
347            stop_distance: None,
348            limit_level: None,
349            limit_distance: None,
350            expiry: expiry.clone(),
351            deal_reference: deal_reference.clone(),
352            force_open,
353            currency_code,
354            quote_id: None,
355            trailing_stop: Some(false),
356            trailing_stop_increment: None,
357        }
358    }
359
360    /// Creates a new instance of an order to buy an option in the market with specified parameters.
361    ///
362    /// This method initializes an order with the following default values:
363    /// - `direction` is set to `Buy`.
364    /// - `order_type` is set to `Limit`.
365    /// - `time_in_force` is set to `FillOrKill`.
366    /// - `level` is set to `Some(DEFAULT_ORDER_BUY_SIZE)`.
367    /// - `force_open` is set to `true`.
368    ///   Other optional parameters, such as stop levels, distances, expiry, and currency code, are left as `None`.
369    ///
370    /// # Parameters
371    /// - `epic` (`String`): The identifier for the market or instrument to trade.
372    /// - `size` (`f64`): The size or quantity of the order to be executed.
373    ///
374    /// # Returns
375    /// A new instance of `Self` that represents the configured buy option for the given market.
376    ///
377    /// # Note
378    /// Ensure the `epic` and `size` values provided are valid and match required market conditions.
379    pub fn buy_option_to_market(
380        epic: String,
381        size: f64,
382        expiry: Option<String>,
383        deal_reference: Option<String>,
384        currency_code: Option<String>,
385    ) -> Self {
386        let rounded_size = (size * 100.0).floor() / 100.0;
387
388        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
389
390        let deal_reference =
391            deal_reference.or_else(|| Some(nanoid::nanoid!(30, &nanoid::alphabet::SAFE)));
392
393        Self {
394            epic,
395            direction: Direction::Buy,
396            size: rounded_size,
397            order_type: OrderType::Limit,
398            time_in_force: TimeInForce::FillOrKill,
399            level: Some(DEFAULT_ORDER_BUY_LEVEL),
400            guaranteed_stop: false,
401            stop_level: None,
402            stop_distance: None,
403            limit_level: None,
404            limit_distance: None,
405            expiry: expiry.clone(),
406            deal_reference: deal_reference.clone(),
407            force_open: true,
408            currency_code: currency_code.clone(),
409            quote_id: None,
410            trailing_stop: Some(false),
411            trailing_stop_increment: None,
412        }
413    }
414
415    /// Constructs a new instance of an order to buy an option in the market with optional force_open behavior.
416    ///
417    /// # Parameters
418    ///
419    /// * `epic` - A `String` representing the unique identifier of the instrument to be traded.
420    /// * `size` - A `f64` value that represents the size of the order.
421    /// * `expiry` - An optional `String` representing the expiry date of the option.
422    /// * `deal_reference` - An optional `String` for the deal reference identifier.
423    /// * `currency_code` - An optional `String` representing the currency in which the order is denominated.
424    ///   Defaults to "EUR" if not provided.
425    /// * `force_open` - A `bool` indicating whether to force open a new position regardless of existing positions.
426    ///
427    /// # Returns
428    ///
429    /// Returns a new instance of `Self`, representing the constructed order with the provided parameters.
430    ///
431    /// # Behavior
432    ///
433    /// * The size of the order will be rounded down to two decimal places for precision.
434    /// * If a `currency_code` is not provided, the default currency code "EUR" is used.
435    /// * Other parameters are directly mapped into the returned instance.
436    ///
437    /// # Notes
438    ///
439    /// * This function assumes that other order-related fields such as `level`, `stop_level`, `stop_distance`,
440    ///   etc., are set to their defaults or require specific business logic, such as
441    ///   `DEFAULT_ORDER_BUY_SIZE` for the initial buy size.
442    pub fn buy_option_to_market_w_force(
443        epic: String,
444        size: f64,
445        expiry: Option<String>,
446        deal_reference: Option<String>,
447        currency_code: Option<String>,
448        force_open: bool,
449    ) -> Self {
450        let rounded_size = (size * 100.0).floor() / 100.0;
451
452        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
453
454        let deal_reference =
455            deal_reference.or_else(|| Some(nanoid::nanoid!(30, &nanoid::alphabet::SAFE)));
456
457        Self {
458            epic,
459            direction: Direction::Buy,
460            size: rounded_size,
461            order_type: OrderType::Limit,
462            time_in_force: TimeInForce::FillOrKill,
463            level: Some(DEFAULT_ORDER_BUY_LEVEL),
464            guaranteed_stop: false,
465            stop_level: None,
466            stop_distance: None,
467            limit_level: None,
468            limit_distance: None,
469            expiry: expiry.clone(),
470            deal_reference: deal_reference.clone(),
471            force_open,
472            currency_code: currency_code.clone(),
473            quote_id: None,
474            trailing_stop: Some(false),
475            trailing_stop_increment: None,
476        }
477    }
478
479    /// Adds a stop loss to the order
480    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
481        self.stop_level = Some(stop_level);
482        self
483    }
484
485    /// Adds a take profit to the order
486    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
487        self.limit_level = Some(limit_level);
488        self
489    }
490
491    /// Adds a trailing stop loss to the order
492    pub fn with_trailing_stop_loss(mut self, trailing_stop_increment: f64) -> Self {
493        self.trailing_stop = Some(true);
494        self.trailing_stop_increment = Some(trailing_stop_increment);
495        self
496    }
497
498    /// Adds a reference to the order
499    pub fn with_reference(mut self, reference: String) -> Self {
500        self.deal_reference = Some(reference);
501        self
502    }
503
504    /// Adds a stop distance to the order
505    pub fn with_stop_distance(mut self, stop_distance: f64) -> Self {
506        self.stop_distance = Some(stop_distance);
507        self
508    }
509
510    /// Adds a limit distance to the order
511    pub fn with_limit_distance(mut self, limit_distance: f64) -> Self {
512        self.limit_distance = Some(limit_distance);
513        self
514    }
515
516    /// Adds a guaranteed stop to the order
517    pub fn with_guaranteed_stop(mut self, guaranteed: bool) -> Self {
518        self.guaranteed_stop = guaranteed;
519        self
520    }
521}
522
523/// Model for updating an existing position (PUT /positions/otc/{dealId})
524///
525/// # Constraints
526/// - If `guaranteed_stop` is `true`, then `stop_level` must be set
527/// - If `guaranteed_stop` is `true`, then `trailing_stop` must be `false`
528/// - If `trailing_stop` is `false`, then DO NOT set `trailing_stop_distance` or `trailing_stop_increment`
529/// - If `trailing_stop` is `true`, then `guaranteed_stop` must be `false`
530/// - If `trailing_stop` is `true`, then `trailing_stop_distance`, `trailing_stop_increment`, and `stop_level` must be set
531#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
532pub struct UpdatePositionRequest {
533    /// True if a guaranteed stop is required
534    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
535    pub guaranteed_stop: Option<bool>,
536    /// New price level for take profit
537    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
538    pub limit_level: Option<f64>,
539    /// New price level for stop loss
540    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
541    pub stop_level: Option<f64>,
542    /// True if trailing stop is required
543    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
544    pub trailing_stop: Option<bool>,
545    /// Distance for trailing stop in points
546    #[serde(
547        rename = "trailingStopDistance",
548        skip_serializing_if = "Option::is_none"
549    )]
550    pub trailing_stop_distance: Option<f64>,
551    /// Trailing stop step increment in points
552    #[serde(
553        rename = "trailingStopIncrement",
554        skip_serializing_if = "Option::is_none"
555    )]
556    pub trailing_stop_increment: Option<f64>,
557}
558
559/// Model for closing an existing position
560#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
561pub struct ClosePositionRequest {
562    /// Unique identifier for the position to close
563    #[serde(rename = "dealId", skip_serializing_if = "Option::is_none")]
564    pub deal_id: Option<String>,
565    /// Direction of the closing order (opposite to the position)
566    pub direction: Direction,
567    /// Instrument EPIC identifier
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub epic: Option<String>,
570    /// Expiry date for the order
571    #[serde(rename = "expiry", skip_serializing_if = "Option::is_none")]
572    pub expiry: Option<String>,
573    /// Price level for limit close orders
574    #[serde(rename = "level", skip_serializing_if = "Option::is_none")]
575    pub level: Option<f64>,
576    /// Type of order to use for closing
577    #[serde(rename = "orderType")]
578    pub order_type: OrderType,
579    /// Quote identifier for the order, used for certain order types that require a specific quote
580    #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")]
581    pub quote_id: Option<String>,
582    /// Size/quantity to close
583    pub size: f64,
584    /// Order duration for the closing order
585    #[serde(rename = "timeInForce")]
586    pub time_in_force: TimeInForce,
587}
588
589impl ClosePositionRequest {
590    /// Creates a request to close a position at market price
591    pub fn market(deal_id: String, direction: Direction, size: f64) -> Self {
592        Self {
593            deal_id: Some(deal_id),
594            direction,
595            size,
596            order_type: OrderType::Market,
597            time_in_force: TimeInForce::FillOrKill,
598            level: None,
599            expiry: None,
600            epic: None,
601            quote_id: None,
602        }
603    }
604
605    /// Creates a request to close a position at a specific price level
606    ///
607    /// This is useful for instruments that don't support market orders
608    pub fn limit(deal_id: String, direction: Direction, size: f64, level: f64) -> Self {
609        Self {
610            deal_id: Some(deal_id),
611            direction,
612            size,
613            order_type: OrderType::Limit,
614            time_in_force: TimeInForce::FillOrKill,
615            level: Some(level),
616            expiry: None,
617            epic: None,
618            quote_id: None,
619        }
620    }
621
622    /// Creates a request to close an option position by deal ID using a limit order with predefined price levels
623    ///
624    /// This is specifically designed for options trading where market orders are not supported
625    /// and a limit order with a predefined price level is required based on the direction.
626    ///
627    /// # Arguments
628    /// * `deal_id` - The ID of the deal to close
629    /// * `direction` - The direction of the closing order (opposite of the position direction)
630    /// * `size` - The size of the position to close
631    pub fn close_option_to_market_by_id(deal_id: String, direction: Direction, size: f64) -> Self {
632        // For options, we need to use limit orders with appropriate levels
633        // Use reasonable levels based on direction to ensure fill while being accepted
634        let level = match direction {
635            Direction::Buy => Some(DEFAULT_ORDER_BUY_LEVEL),
636            Direction::Sell => Some(DEFAULT_ORDER_SELL_LEVEL),
637        };
638
639        Self {
640            deal_id: Some(deal_id),
641            direction,
642            size,
643            order_type: OrderType::Limit,
644            time_in_force: TimeInForce::FillOrKill,
645            level,
646            expiry: None,
647            epic: None,
648            quote_id: None,
649        }
650    }
651
652    /// Creates a request to close an option position by epic identifier using a limit order with predefined price levels
653    ///
654    /// This is specifically designed for options trading where market orders are not supported
655    /// and a limit order with a predefined price level is required based on the direction.
656    /// This method is used when the deal ID is not available but the epic and expiry are known.
657    ///
658    /// # Arguments
659    /// * `epic` - The epic identifier of the instrument
660    /// * `expiry` - The expiry date of the option
661    /// * `direction` - The direction of the closing order (opposite of the position direction)
662    /// * `size` - The size of the position to close
663    pub fn close_option_to_market_by_epic(
664        epic: String,
665        expiry: String,
666        direction: Direction,
667        size: f64,
668    ) -> Self {
669        // For options, we need to use limit orders with appropriate levels
670        // Use reasonable levels based on direction to ensure fill while being accepted
671        let level = match direction {
672            Direction::Buy => Some(DEFAULT_ORDER_BUY_LEVEL),
673            Direction::Sell => Some(DEFAULT_ORDER_SELL_LEVEL),
674        };
675
676        Self {
677            deal_id: None,
678            direction,
679            size,
680            order_type: OrderType::Limit,
681            time_in_force: TimeInForce::FillOrKill,
682            level,
683            expiry: Some(expiry),
684            epic: Some(epic),
685            quote_id: None,
686        }
687    }
688}
689
690/// Model for creating a new working order
691#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
692pub struct CreateWorkingOrderRequest {
693    /// Instrument EPIC identifier
694    pub epic: String,
695    /// Order direction (buy or sell)
696    pub direction: Direction,
697    /// Order size/quantity
698    pub size: f64,
699    /// Price level for the order
700    pub level: f64,
701    /// Type of working order (LIMIT or STOP)
702    #[serde(rename = "type")]
703    pub order_type: OrderType,
704    /// Order duration (how long the order remains valid)
705    #[serde(rename = "timeInForce")]
706    pub time_in_force: TimeInForce,
707    /// Whether to use a guaranteed stop
708    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
709    pub guaranteed_stop: Option<bool>,
710    /// Price level for stop loss
711    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
712    pub stop_level: Option<f64>,
713    /// Distance for stop loss
714    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
715    pub stop_distance: Option<f64>,
716    /// Price level for take profit
717    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
718    pub limit_level: Option<f64>,
719    /// Distance for take profit
720    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
721    pub limit_distance: Option<f64>,
722    /// Expiry date for GTD orders
723    #[serde(rename = "goodTillDate", skip_serializing_if = "Option::is_none")]
724    pub good_till_date: Option<String>,
725    /// Client-generated reference for the deal
726    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
727    pub deal_reference: Option<String>,
728    /// Currency code for the order (e.g., "USD", "EUR")
729    #[serde(rename = "currencyCode")]
730    pub currency_code: String,
731    /// Expiry date for the order
732    pub expiry: String,
733}
734
735impl From<WorkingOrder> for CreateWorkingOrderRequest {
736    fn from(value: WorkingOrder) -> Self {
737        let data = value.working_order_data;
738        Self {
739            epic: data.epic,
740            direction: data.direction,
741            size: data.order_size,
742            level: data.order_level,
743            order_type: data.order_type,
744            time_in_force: data.time_in_force,
745            guaranteed_stop: Some(data.guaranteed_stop),
746            stop_level: data.stop_level,
747            stop_distance: data.stop_distance,
748            limit_level: data.limit_level,
749            limit_distance: data.limit_distance,
750            good_till_date: data.good_till_date,
751            deal_reference: data.deal_reference,
752            currency_code: data.currency_code,
753            expiry: value.market_data.expiry,
754        }
755    }
756}
757
758impl CreateWorkingOrderRequest {
759    /// Creates a new limit working order
760    pub fn limit(
761        epic: String,
762        direction: Direction,
763        size: f64,
764        level: f64,
765        currency_code: String,
766        expiry: String,
767    ) -> Self {
768        Self {
769            epic,
770            direction,
771            size,
772            level,
773            order_type: OrderType::Limit,
774            time_in_force: TimeInForce::GoodTillCancelled,
775            guaranteed_stop: Some(false),
776            stop_level: None,
777            stop_distance: None,
778            limit_level: None,
779            limit_distance: None,
780            good_till_date: None,
781            deal_reference: None,
782            currency_code,
783            expiry,
784        }
785    }
786
787    /// Creates a new stop working order
788    pub fn stop(
789        epic: String,
790        direction: Direction,
791        size: f64,
792        level: f64,
793        currency_code: String,
794        expiry: String,
795    ) -> Self {
796        Self {
797            epic,
798            direction,
799            size,
800            level,
801            order_type: OrderType::Stop,
802            time_in_force: TimeInForce::GoodTillCancelled,
803            guaranteed_stop: Some(false),
804            stop_level: None,
805            stop_distance: None,
806            limit_level: None,
807            limit_distance: None,
808            good_till_date: None,
809            deal_reference: None,
810            currency_code,
811            expiry,
812        }
813    }
814
815    /// Adds a stop loss to the working order
816    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
817        self.stop_level = Some(stop_level);
818        self
819    }
820
821    /// Adds a take profit to the working order
822    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
823        self.limit_level = Some(limit_level);
824        self
825    }
826
827    /// Adds a reference to the working order
828    pub fn with_reference(mut self, reference: String) -> Self {
829        self.deal_reference = Some(reference);
830        self
831    }
832
833    /// Sets the expiration date for an order and updates the time-in-force policy.
834    ///
835    /// This method updates the `time_in_force` property to `GoodTillDate` and assigns
836    /// the provided expiration date to the `good_till_date` property. It allows chaining
837    /// as it consumes the current instance and returns it after modification.
838    ///
839    /// # Arguments
840    /// * `date` - A `String` representing the expiration date for the order.
841    ///
842    /// # Returns
843    /// * `Self` - The updated instance of the type, allowing method chaining.
844    ///
845    /// In the example above, the expiration date for the order is set to
846    /// "2023-12-31T23:59:59Z" and the `time_in_force` policy is set to `GoodTillDate`.
847    pub fn expires_at(mut self, date: String) -> Self {
848        self.time_in_force = TimeInForce::GoodTillDate;
849        self.good_till_date = Some(date);
850        self
851    }
852
853    ///
854    /// Sets the order to expire by the end of the next day (tomorrow).
855    ///
856    /// This method modifies the `time_in_force` field to `GoodTillDate` and calculates
857    /// the expiration date as tomorrow's date and time. The calculated date is then
858    /// formatted as a string in the format `YYYY/MM/DD HH:MM:SS` and assigned to
859    /// the `good_till_date` field.
860    ///
861    /// # Returns
862    /// Returns the updated instance of the struct with the expiration date set to tomorrow.
863    ///
864    /// In this example, the `expires_tomorrow` method sets the order to expire at the
865    /// same time on the next calendar day.
866    ///
867    /// Note: The function uses the UTC timezone for calculating the date and time.
868    ///
869    pub fn expires_tomorrow(mut self) -> Self {
870        self.time_in_force = TimeInForce::GoodTillDate;
871        let tomorrow = Utc::now() + Duration::days(1);
872        self.good_till_date = Some(tomorrow.format("%Y/%m/%d %H:%M:%S").to_string());
873        self
874    }
875
876    /// Sets the expiration time for an order and configures it to expire at a specific date and time.
877    ///
878    /// This method modifies the `time_in_force` for the order to `GoodTillDate`
879    /// and calculates the expiration time by adding the provided `duration` to
880    /// the current UTC time. The calculated expiration time is formatted as
881    /// "YYYY/MM/DD HH:MM:SS" and stored in the `good_till_date` field.
882    ///
883    /// # Parameters
884    /// - `duration`: A `Duration` instance that represents the amount of time
885    ///   after the current UTC time when the order should expire.
886    ///
887    /// # Returns
888    /// Returns `Self` with updated `time_in_force` and `good_till_date`.
889    ///
890    /// Note: This method assumes that the runtime uses the `chrono` crate for
891    /// time handling and formatting.
892    pub fn expires_in(mut self, duration: Duration) -> Self {
893        self.time_in_force = TimeInForce::GoodTillDate;
894        let tomorrow = Utc::now() + duration;
895        self.good_till_date = Some(tomorrow.format("%Y/%m/%d %H:%M:%S").to_string());
896        self
897    }
898}