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