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