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