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