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