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