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
524#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
525pub struct UpdatePositionRequest {
526    /// New price level for stop loss
527    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
528    pub stop_level: Option<f64>,
529    /// New price level for take profit
530    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
531    pub limit_level: Option<f64>,
532    /// Whether to enable trailing stop
533    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
534    pub trailing_stop: Option<bool>,
535    /// Distance for trailing stop
536    #[serde(
537        rename = "trailingStopDistance",
538        skip_serializing_if = "Option::is_none"
539    )]
540    pub trailing_stop_distance: Option<f64>,
541}
542
543/// Model for closing an existing position
544#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
545pub struct ClosePositionRequest {
546    /// Unique identifier for the position to close
547    #[serde(rename = "dealId", skip_serializing_if = "Option::is_none")]
548    pub deal_id: Option<String>,
549    /// Direction of the closing order (opposite to the position)
550    pub direction: Direction,
551    /// Instrument EPIC identifier
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub epic: Option<String>,
554    /// Expiry date for the order
555    #[serde(rename = "expiry", skip_serializing_if = "Option::is_none")]
556    pub expiry: Option<String>,
557    /// Price level for limit close orders
558    #[serde(rename = "level", skip_serializing_if = "Option::is_none")]
559    pub level: Option<f64>,
560    /// Type of order to use for closing
561    #[serde(rename = "orderType")]
562    pub order_type: OrderType,
563    /// Quote identifier for the order, used for certain order types that require a specific quote
564    #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")]
565    pub quote_id: Option<String>,
566    /// Size/quantity to close
567    pub size: f64,
568    /// Order duration for the closing order
569    #[serde(rename = "timeInForce")]
570    pub time_in_force: TimeInForce,
571}
572
573impl ClosePositionRequest {
574    /// Creates a request to close a position at market price
575    pub fn market(deal_id: String, direction: Direction, size: f64) -> Self {
576        Self {
577            deal_id: Some(deal_id),
578            direction,
579            size,
580            order_type: OrderType::Market,
581            time_in_force: TimeInForce::FillOrKill,
582            level: None,
583            expiry: None,
584            epic: None,
585            quote_id: None,
586        }
587    }
588
589    /// Creates a request to close a position at a specific price level
590    ///
591    /// This is useful for instruments that don't support market orders
592    pub fn limit(deal_id: String, direction: Direction, size: f64, level: f64) -> Self {
593        Self {
594            deal_id: Some(deal_id),
595            direction,
596            size,
597            order_type: OrderType::Limit,
598            time_in_force: TimeInForce::FillOrKill,
599            level: Some(level),
600            expiry: None,
601            epic: None,
602            quote_id: None,
603        }
604    }
605
606    /// Creates a request to close an option position by deal ID using a limit order with predefined price levels
607    ///
608    /// This is specifically designed for options trading where market orders are not supported
609    /// and a limit order with a predefined price level is required based on the direction.
610    ///
611    /// # Arguments
612    /// * `deal_id` - The ID of the deal to close
613    /// * `direction` - The direction of the closing order (opposite of the position direction)
614    /// * `size` - The size of the position to close
615    pub fn close_option_to_market_by_id(deal_id: String, direction: Direction, size: f64) -> Self {
616        // For options, we need to use limit orders with appropriate levels
617        // Use reasonable levels based on direction to ensure fill while being accepted
618        let level = match direction {
619            Direction::Buy => Some(DEFAULT_ORDER_BUY_LEVEL),
620            Direction::Sell => Some(DEFAULT_ORDER_SELL_LEVEL),
621        };
622
623        Self {
624            deal_id: Some(deal_id),
625            direction,
626            size,
627            order_type: OrderType::Limit,
628            time_in_force: TimeInForce::FillOrKill,
629            level,
630            expiry: None,
631            epic: None,
632            quote_id: None,
633        }
634    }
635
636    /// Creates a request to close an option position by epic identifier using a limit order with predefined price levels
637    ///
638    /// This is specifically designed for options trading where market orders are not supported
639    /// and a limit order with a predefined price level is required based on the direction.
640    /// This method is used when the deal ID is not available but the epic and expiry are known.
641    ///
642    /// # Arguments
643    /// * `epic` - The epic identifier of the instrument
644    /// * `expiry` - The expiry date of the option
645    /// * `direction` - The direction of the closing order (opposite of the position direction)
646    /// * `size` - The size of the position to close
647    pub fn close_option_to_market_by_epic(
648        epic: String,
649        expiry: String,
650        direction: Direction,
651        size: f64,
652    ) -> Self {
653        // For options, we need to use limit orders with appropriate levels
654        // Use reasonable levels based on direction to ensure fill while being accepted
655        let level = match direction {
656            Direction::Buy => Some(DEFAULT_ORDER_BUY_LEVEL),
657            Direction::Sell => Some(DEFAULT_ORDER_SELL_LEVEL),
658        };
659
660        Self {
661            deal_id: None,
662            direction,
663            size,
664            order_type: OrderType::Limit,
665            time_in_force: TimeInForce::FillOrKill,
666            level,
667            expiry: Some(expiry),
668            epic: Some(epic),
669            quote_id: None,
670        }
671    }
672}
673
674/// Model for creating a new working order
675#[derive(DebugPretty, DisplaySimple, Clone, Deserialize, Serialize, Default)]
676pub struct CreateWorkingOrderRequest {
677    /// Instrument EPIC identifier
678    pub epic: String,
679    /// Order direction (buy or sell)
680    pub direction: Direction,
681    /// Order size/quantity
682    pub size: f64,
683    /// Price level for the order
684    pub level: f64,
685    /// Type of working order (LIMIT or STOP)
686    #[serde(rename = "type")]
687    pub order_type: OrderType,
688    /// Order duration (how long the order remains valid)
689    #[serde(rename = "timeInForce")]
690    pub time_in_force: TimeInForce,
691    /// Whether to use a guaranteed stop
692    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
693    pub guaranteed_stop: Option<bool>,
694    /// Price level for stop loss
695    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
696    pub stop_level: Option<f64>,
697    /// Distance for stop loss
698    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
699    pub stop_distance: Option<f64>,
700    /// Price level for take profit
701    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
702    pub limit_level: Option<f64>,
703    /// Distance for take profit
704    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
705    pub limit_distance: Option<f64>,
706    /// Expiry date for GTD orders
707    #[serde(rename = "goodTillDate", skip_serializing_if = "Option::is_none")]
708    pub good_till_date: Option<String>,
709    /// Client-generated reference for the deal
710    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
711    pub deal_reference: Option<String>,
712    /// Currency code for the order (e.g., "USD", "EUR")
713    #[serde(rename = "currencyCode")]
714    pub currency_code: String,
715    /// Expiry date for the order
716    pub expiry: String,
717}
718
719impl From<WorkingOrder> for CreateWorkingOrderRequest {
720    fn from(value: WorkingOrder) -> Self {
721        let data = value.working_order_data;
722        Self {
723            epic: data.epic,
724            direction: data.direction,
725            size: data.order_size,
726            level: data.order_level,
727            order_type: data.order_type,
728            time_in_force: data.time_in_force,
729            guaranteed_stop: Some(data.guaranteed_stop),
730            stop_level: data.stop_level,
731            stop_distance: data.stop_distance,
732            limit_level: data.limit_level,
733            limit_distance: data.limit_distance,
734            good_till_date: data.good_till_date,
735            deal_reference: data.deal_reference,
736            currency_code: data.currency_code,
737            expiry: value.market_data.expiry,
738        }
739    }
740}
741
742impl CreateWorkingOrderRequest {
743    /// Creates a new limit working order
744    pub fn limit(
745        epic: String,
746        direction: Direction,
747        size: f64,
748        level: f64,
749        currency_code: String,
750        expiry: String,
751    ) -> Self {
752        Self {
753            epic,
754            direction,
755            size,
756            level,
757            order_type: OrderType::Limit,
758            time_in_force: TimeInForce::GoodTillCancelled,
759            guaranteed_stop: Some(false),
760            stop_level: None,
761            stop_distance: None,
762            limit_level: None,
763            limit_distance: None,
764            good_till_date: None,
765            deal_reference: None,
766            currency_code,
767            expiry,
768        }
769    }
770
771    /// Creates a new stop working order
772    pub fn stop(
773        epic: String,
774        direction: Direction,
775        size: f64,
776        level: f64,
777        currency_code: String,
778        expiry: String,
779    ) -> Self {
780        Self {
781            epic,
782            direction,
783            size,
784            level,
785            order_type: OrderType::Stop,
786            time_in_force: TimeInForce::GoodTillCancelled,
787            guaranteed_stop: Some(false),
788            stop_level: None,
789            stop_distance: None,
790            limit_level: None,
791            limit_distance: None,
792            good_till_date: None,
793            deal_reference: None,
794            currency_code,
795            expiry,
796        }
797    }
798
799    /// Adds a stop loss to the working order
800    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
801        self.stop_level = Some(stop_level);
802        self
803    }
804
805    /// Adds a take profit to the working order
806    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
807        self.limit_level = Some(limit_level);
808        self
809    }
810
811    /// Adds a reference to the working order
812    pub fn with_reference(mut self, reference: String) -> Self {
813        self.deal_reference = Some(reference);
814        self
815    }
816
817    /// Sets the expiration date for an order and updates the time-in-force policy.
818    ///
819    /// This method updates the `time_in_force` property to `GoodTillDate` and assigns
820    /// the provided expiration date to the `good_till_date` property. It allows chaining
821    /// as it consumes the current instance and returns it after modification.
822    ///
823    /// # Arguments
824    /// * `date` - A `String` representing the expiration date for the order.
825    ///
826    /// # Returns
827    /// * `Self` - The updated instance of the type, allowing method chaining.
828    ///
829    /// In the example above, the expiration date for the order is set to
830    /// "2023-12-31T23:59:59Z" and the `time_in_force` policy is set to `GoodTillDate`.
831    pub fn expires_at(mut self, date: String) -> Self {
832        self.time_in_force = TimeInForce::GoodTillDate;
833        self.good_till_date = Some(date);
834        self
835    }
836
837    ///
838    /// Sets the order to expire by the end of the next day (tomorrow).
839    ///
840    /// This method modifies the `time_in_force` field to `GoodTillDate` and calculates
841    /// the expiration date as tomorrow's date and time. The calculated date is then
842    /// formatted as a string in the format `YYYY/MM/DD HH:MM:SS` and assigned to
843    /// the `good_till_date` field.
844    ///
845    /// # Returns
846    /// Returns the updated instance of the struct with the expiration date set to tomorrow.
847    ///
848    /// In this example, the `expires_tomorrow` method sets the order to expire at the
849    /// same time on the next calendar day.
850    ///
851    /// Note: The function uses the UTC timezone for calculating the date and time.
852    ///
853    pub fn expires_tomorrow(mut self) -> Self {
854        self.time_in_force = TimeInForce::GoodTillDate;
855        let tomorrow = Utc::now() + Duration::days(1);
856        self.good_till_date = Some(tomorrow.format("%Y/%m/%d %H:%M:%S").to_string());
857        self
858    }
859
860    /// Sets the expiration time for an order and configures it to expire at a specific date and time.
861    ///
862    /// This method modifies the `time_in_force` for the order to `GoodTillDate`
863    /// and calculates the expiration time by adding the provided `duration` to
864    /// the current UTC time. The calculated expiration time is formatted as
865    /// "YYYY/MM/DD HH:MM:SS" and stored in the `good_till_date` field.
866    ///
867    /// # Parameters
868    /// - `duration`: A `Duration` instance that represents the amount of time
869    ///   after the current UTC time when the order should expire.
870    ///
871    /// # Returns
872    /// Returns `Self` with updated `time_in_force` and `good_till_date`.
873    ///
874    /// Note: This method assumes that the runtime uses the `chrono` crate for
875    /// time handling and formatting.
876    pub fn expires_in(mut self, duration: Duration) -> Self {
877        self.time_in_force = TimeInForce::GoodTillDate;
878        let tomorrow = Utc::now() + duration;
879        self.good_till_date = Some(tomorrow.format("%Y/%m/%d %H:%M:%S").to_string());
880        self
881    }
882}