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