ig_client/application/models/
order.rs

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