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(skip_serializing_if = "Option::is_none")]
117    pub level: Option<f64>,
118    /// Whether to use a guaranteed stop
119    #[serde(rename = "guaranteedStop")]
120    pub guaranteed_stop: bool,
121    /// Price level for stop loss
122    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
123    pub stop_level: Option<f64>,
124    /// Stop loss distance
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    /// Take profit distance
131    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
132    pub limit_distance: Option<f64>,
133    /// Expiry date for the order
134    #[serde(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")]
141    pub force_open: bool,
142    /// Currency code for the order (e.g., "USD", "EUR")
143    #[serde(rename = "currencyCode")]
144    pub currency_code: String,
145    /// Quote identifier for the order
146    #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")]
147    pub quote_id: Option<String>,
148    /// Trailing stop enabled
149    #[serde(rename = "trailingStop")]
150    pub trailing_stop: bool,
151    /// Trailing stop increment (only if trailingStop is true)
152    #[serde(
153        rename = "trailingStopIncrement",
154        skip_serializing_if = "Option::is_none"
155    )]
156    pub trailing_stop_increment: Option<f64>,
157}
158
159impl CreateOrderRequest {
160    /// Creates a new market order, typically used for CFD (Contract for Difference) accounts
161    pub fn market(
162        epic: String,
163        direction: Direction,
164        size: f64,
165        currency_code: Option<String>,
166    ) -> Self {
167        let rounded_size = (size * 100.0).floor() / 100.0;
168
169        let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
170
171        Self {
172            epic,
173            direction,
174            size: rounded_size,
175            order_type: OrderType::Market,
176            time_in_force: TimeInForce::FillOrKill,
177            level: None,
178            guaranteed_stop: false,
179            stop_level: None,
180            stop_distance: None,
181            limit_level: None,
182            limit_distance: None,
183            expiry: Some("-".to_string()),
184            deal_reference: None,
185            force_open: true,
186            currency_code,
187            quote_id: None,
188            trailing_stop: false,
189            trailing_stop_increment: None,
190        }
191    }
192
193    /// Creates a new limit order, typically used for CFD (Contract for Difference) accounts
194    pub fn limit(
195        epic: String,
196        direction: Direction,
197        size: f64,
198        level: f64,
199        currency_code: 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: None,
219            force_open: true,
220            currency_code,
221            quote_id: None,
222            trailing_stop: 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: 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: 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: 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: 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 = 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, 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, 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, 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, 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, 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, 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, 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, Serialize, Deserialize)]
876pub struct CreateWorkingOrderResponse {
877    /// Client-generated reference for the deal
878    #[serde(rename = "dealReference")]
879    pub deal_reference: String,
880}
881
882impl_json_display!(
883    CreateOrderRequest,
884    CreateOrderResponse,
885    OrderConfirmation,
886    UpdatePositionRequest,
887    ClosePositionRequest,
888    ClosePositionResponse,
889    UpdatePositionResponse,
890    CreateWorkingOrderRequest,
891    CreateWorkingOrderResponse
892);