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