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::presentation::account::WorkingOrder;
8use crate::presentation::order::{Direction, OrderType, TimeInForce};
9use chrono::{Duration, Utc};
10use pretty_simple_display::{DebugPretty, DisplaySimple};
11use serde::{Deserialize, Serialize};
12use std::fmt;
13use std::fmt::{Debug, Display};
14
15/// Parameters for getting recent prices (API v3)
16#[derive(Clone, Default, Deserialize, Serialize)]
17pub struct RecentPricesRequest<'a> {
18 /// Instrument epic
19 pub epic: &'a str,
20 /// Optional price resolution (default: MINUTE)
21 pub resolution: Option<&'a str>,
22 /// Optional start date time (yyyy-MM-dd'T'HH:mm:ss)
23 pub from: Option<&'a str>,
24 /// Optional end date time (yyyy-MM-dd'T'HH:mm:ss)
25 pub to: Option<&'a str>,
26 /// Optional max number of price points (default: 10)
27 #[serde(rename = "max")]
28 pub max_points: Option<u32>,
29 /// Optional page size (default: 20, disable paging = 0)
30 #[serde(rename = "pageSize")]
31 pub page_size: Option<u32>,
32 /// Optional page number (default: 1)
33 #[serde(rename = "pageNumber")]
34 pub page_number: Option<u32>,
35}
36
37impl<'a> RecentPricesRequest<'a> {
38 /// Create new parameters with just the epic (required field)
39 #[must_use]
40 pub fn new(epic: &'a str) -> Self {
41 Self {
42 epic,
43 ..Default::default()
44 }
45 }
46
47 /// Set the resolution
48 #[must_use]
49 pub fn with_resolution(mut self, resolution: &'a str) -> Self {
50 self.resolution = Some(resolution);
51 self
52 }
53
54 /// Set the from date
55 #[must_use]
56 pub fn with_from(mut self, from: &'a str) -> Self {
57 self.from = Some(from);
58 self
59 }
60
61 /// Set the to date
62 #[must_use]
63 pub fn with_to(mut self, to: &'a str) -> Self {
64 self.to = Some(to);
65 self
66 }
67
68 /// Set the max points
69 #[must_use]
70 pub fn with_max_points(mut self, max_points: u32) -> Self {
71 self.max_points = Some(max_points);
72 self
73 }
74
75 /// Set the page size
76 #[must_use]
77 pub fn with_page_size(mut self, page_size: u32) -> Self {
78 self.page_size = Some(page_size);
79 self
80 }
81
82 /// Set the page number
83 #[must_use]
84 pub fn with_page_number(mut self, page_number: u32) -> Self {
85 self.page_number = Some(page_number);
86 self
87 }
88}
89
90impl Display for RecentPricesRequest<'_> {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 let json = serde_json::to_string(self).unwrap_or_else(|_| "Invalid JSON".to_string());
93 write!(f, "{}", json)
94 }
95}
96
97impl Debug for RecentPricesRequest<'_> {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 let json =
100 serde_json::to_string_pretty(self).unwrap_or_else(|_| "Invalid JSON".to_string());
101 write!(f, "{}", json)
102 }
103}
104
105/// Model for creating a new order
106#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
107pub struct CreateOrderRequest {
108 /// Instrument EPIC identifier
109 pub epic: String,
110 /// Order direction (buy or sell)
111 pub direction: Direction,
112 /// Order size/quantity
113 pub size: f64,
114 /// Type of order (market, limit, etc.)
115 #[serde(rename = "orderType")]
116 pub order_type: OrderType,
117 /// Order duration (how long the order remains valid)
118 #[serde(rename = "timeInForce")]
119 pub time_in_force: TimeInForce,
120 /// Price level for limit orders
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub level: Option<f64>,
123 /// Whether to use a guaranteed stop
124 #[serde(rename = "guaranteedStop")]
125 pub guaranteed_stop: bool,
126 /// Price level for stop loss
127 #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
128 pub stop_level: Option<f64>,
129 /// Stop loss distance
130 #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
131 pub stop_distance: Option<f64>,
132 /// Price level for take profit
133 #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
134 pub limit_level: Option<f64>,
135 /// Take profit distance
136 #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
137 pub limit_distance: Option<f64>,
138 /// Expiry date for the order
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub expiry: Option<String>,
141 /// Client-generated reference for the deal
142 #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
143 pub deal_reference: Option<String>,
144 /// Whether to force open a new position
145 #[serde(rename = "forceOpen")]
146 pub force_open: bool,
147 /// Currency code for the order (e.g., "USD", "EUR")
148 #[serde(rename = "currencyCode")]
149 pub currency_code: String,
150 /// Quote identifier for the order
151 #[serde(rename = "quoteId", skip_serializing_if = "Option::is_none")]
152 pub quote_id: Option<String>,
153 /// Trailing stop enabled
154 #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
155 pub trailing_stop: Option<bool>,
156 /// Trailing stop increment (only if trailingStop is true)
157 #[serde(
158 rename = "trailingStopIncrement",
159 skip_serializing_if = "Option::is_none"
160 )]
161 pub trailing_stop_increment: Option<f64>,
162}
163
164/// Rounds an order size to two decimal places (nearest, half away from zero).
165///
166/// IG accepts order sizes to two decimals. Rounding must go through the nearest
167/// value, not `floor`: `0.29 * 100.0` is `28.999999999999996` in `f64`, so a
168/// `floor`-based `(size * 100.0).floor() / 100.0` silently yields `0.28` and the
169/// order is placed with a smaller size than the caller requested. `round` maps
170/// it back to `0.29`. The rounding direction (nearest) is deliberate and applies
171/// uniformly to every order constructor.
172#[must_use]
173#[inline]
174fn round_order_size(size: f64) -> f64 {
175 (size * 100.0).round() / 100.0
176}
177
178impl CreateOrderRequest {
179 /// Creates a new market order, typically used for CFD (Contract for Difference) accounts
180 #[must_use]
181 pub fn market(
182 epic: String,
183 direction: Direction,
184 size: f64,
185 currency_code: Option<String>,
186 deal_reference: Option<String>,
187 ) -> Self {
188 let rounded_size = round_order_size(size);
189
190 let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
191
192 Self {
193 epic,
194 direction,
195 size: rounded_size,
196 order_type: OrderType::Market,
197 time_in_force: TimeInForce::FillOrKill,
198 level: None,
199 guaranteed_stop: false,
200 stop_level: None,
201 stop_distance: None,
202 limit_level: None,
203 limit_distance: None,
204 expiry: Some("-".to_string()),
205 deal_reference,
206 force_open: true,
207 currency_code,
208 quote_id: None,
209 trailing_stop: Some(false),
210 trailing_stop_increment: None,
211 }
212 }
213
214 /// Creates a new limit order, typically used for CFD (Contract for Difference) accounts
215 #[must_use]
216 pub fn limit(
217 epic: String,
218 direction: Direction,
219 size: f64,
220 level: f64,
221 currency_code: Option<String>,
222 deal_reference: Option<String>,
223 ) -> Self {
224 let rounded_size = round_order_size(size);
225
226 let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
227
228 Self {
229 epic,
230 direction,
231 size: rounded_size,
232 order_type: OrderType::Limit,
233 time_in_force: TimeInForce::GoodTillCancelled,
234 level: Some(level),
235 guaranteed_stop: false,
236 stop_level: None,
237 stop_distance: None,
238 limit_level: None,
239 limit_distance: None,
240 expiry: None,
241 deal_reference,
242 force_open: true,
243 currency_code,
244 quote_id: None,
245 trailing_stop: Some(false),
246 trailing_stop_increment: None,
247 }
248 }
249
250 /// Creates a new instance of a market sell option with predefined parameters.
251 ///
252 /// This function sets up a sell option to the market for a given asset (`epic`)
253 /// with the specified size. It configures the order with default values
254 /// for attributes such as direction, order type, and time-in-force.
255 ///
256 /// # Parameters
257 /// - `epic`: A `String` that represents the epic (unique identifier or code) of the instrument
258 /// being traded.
259 /// - `size`: A `f64` value representing the size or quantity of the order.
260 ///
261 /// # Returns
262 /// An instance of `Self` (the type implementing this function), containing the specified
263 /// `epic` and `size`, along with default values for other parameters:
264 ///
265 /// - `direction`: Set to `Direction::Sell`.
266 /// - `order_type`: Set to `OrderType::Limit`.
267 /// - `time_in_force`: Set to `TimeInForce::FillOrKill`.
268 /// - `level`: Set to `Some(DEFAULT_ORDER_SELL_SIZE)`.
269 /// - `guaranteed_stop`: Set to `false`.
270 /// - `stop_level`: Set to `None`.
271 /// - `stop_distance`: Set to `None`.
272 /// - `limit_level`: Set to `None`.
273 /// - `limit_distance`: Set to `None`.
274 /// - `expiry`: Set based on input or `None`.
275 /// - `deal_reference`: Auto-generated if not provided.
276 /// - `force_open`: Set to `true`.
277 /// - `currency_code`: Defaults to `"EUR"` if not provided.
278 ///
279 /// Note that this function allows for minimal input (the instrument and size),
280 /// while other fields are provided default values. If further customization is required,
281 /// you can modify the returned instance as needed.
282 #[must_use]
283 pub fn sell_option_to_market(
284 epic: String,
285 size: f64,
286 expiry: Option<String>,
287 deal_reference: Option<String>,
288 currency_code: Option<String>,
289 ) -> Self {
290 Self::option_to_market(
291 Direction::Sell,
292 true,
293 epic,
294 size,
295 expiry,
296 deal_reference,
297 currency_code,
298 )
299 }
300
301 /// Shared builder behind the four option-to-market limit-order constructors.
302 ///
303 /// The public `sell_option_to_market` / `buy_option_to_market` variants (with
304 /// and without an explicit `force_open`) differ only by `direction` and
305 /// `force_open`; every other field is identical, so the shared body lives
306 /// here to keep the four wrappers in lock-step. The aggressive limit `level`
307 /// is chosen from `direction` (`DEFAULT_ORDER_SELL_LEVEL` for a sell,
308 /// `DEFAULT_ORDER_BUY_LEVEL` for a buy) so the order fills at market. `size`
309 /// is rounded to two decimals via [`round_order_size`]; a missing
310 /// `deal_reference` is auto-generated, and a missing `currency_code` defaults
311 /// to `"EUR"`.
312 #[must_use]
313 fn option_to_market(
314 direction: Direction,
315 force_open: bool,
316 epic: String,
317 size: f64,
318 expiry: Option<String>,
319 deal_reference: Option<String>,
320 currency_code: Option<String>,
321 ) -> Self {
322 let rounded_size = round_order_size(size);
323
324 let currency_code = currency_code.unwrap_or_else(|| "EUR".to_string());
325
326 let deal_reference = deal_reference.or_else(|| Some(crate::utils::id::get_id()));
327
328 let level = match direction {
329 Direction::Sell => DEFAULT_ORDER_SELL_LEVEL,
330 Direction::Buy => DEFAULT_ORDER_BUY_LEVEL,
331 };
332
333 Self {
334 epic,
335 direction,
336 size: rounded_size,
337 order_type: OrderType::Limit,
338 time_in_force: TimeInForce::FillOrKill,
339 level: Some(level),
340 guaranteed_stop: false,
341 stop_level: None,
342 stop_distance: None,
343 limit_level: None,
344 limit_distance: None,
345 expiry,
346 deal_reference,
347 force_open,
348 currency_code,
349 quote_id: None,
350 trailing_stop: Some(false),
351 trailing_stop_increment: None,
352 }
353 }
354
355 /// Constructs and returns a new instance of the `Self` struct representing a sell option
356 /// to the market with specific parameters for execution.
357 ///
358 /// # Parameters
359 /// - `epic`: A `String` that specifies the EPIC
360 /// (Exchanged Product Information Code) of the instrument for which the sell order is created.
361 /// - `size`: A `f64` that represents the size of the sell
362 /// order. The size is rounded to the nearest two decimal places.
363 /// - `expiry`: An optional `String` that indicates the expiry date or period for
364 /// the sell order. If `None`, no expiry date will be set for the order.
365 /// - `deal_reference`: An optional `String` that contains a reference or identifier
366 /// for the deal. Can be used for tracking purposes.
367 /// - `currency_code`: An optional `String` representing the currency code. Defaults
368 /// to `"EUR"` if not provided.
369 /// - `force_open`: A `bool` that specifies whether to force open the
370 /// position. When `true`, a new position is opened even if an existing position for the
371 /// same instrument and direction is available.
372 ///
373 /// # Returns
374 /// - `Self`: A new instance populated with the provided parameters, including the following default
375 /// properties:
376 /// - `direction`: Set to `Direction::Sell` to designate the sell operation.
377 /// - `order_type`: Set to `OrderType::Limit` to signify the type of the order.
378 /// - `time_in_force`: Set to `TimeInForce::FillOrKill` indicating the order should be fully
379 /// executed or canceled.
380 /// - `level`: Set to a constant value `DEFAULT_ORDER_SELL_SIZE`.
381 /// - `guaranteed_stop`: Set to `false`, indicating no guaranteed stop.
382 /// - Other optional levels/distance fields (`stop_level`, `stop_distance`, `limit_level`,
383 /// `limit_distance`): Set to `None` by default.
384 ///
385 /// # Notes
386 /// - The input `size` is automatically rounded to the nearest two decimal places before being stored.
387 #[must_use]
388 pub fn sell_option_to_market_w_force(
389 epic: String,
390 size: f64,
391 expiry: Option<String>,
392 deal_reference: Option<String>,
393 currency_code: Option<String>,
394 force_open: bool, // Compensate position if it is already open
395 ) -> Self {
396 Self::option_to_market(
397 Direction::Sell,
398 force_open,
399 epic,
400 size,
401 expiry,
402 deal_reference,
403 currency_code,
404 )
405 }
406
407 /// Creates a new instance of an order to buy an option in the market with specified parameters.
408 ///
409 /// This method initializes an order with the following default values:
410 /// - `direction` is set to `Buy`.
411 /// - `order_type` is set to `Limit`.
412 /// - `time_in_force` is set to `FillOrKill`.
413 /// - `level` is set to `Some(DEFAULT_ORDER_BUY_SIZE)`.
414 /// - `force_open` is set to `true`.
415 /// Other optional parameters, such as stop levels, distances, expiry, and currency code, are left as `None`.
416 ///
417 /// # Parameters
418 /// - `epic` (`String`): The identifier for the market or instrument to trade.
419 /// - `size` (`f64`): The size or quantity of the order to be executed.
420 ///
421 /// # Returns
422 /// A new instance of `Self` that represents the configured buy option for the given market.
423 ///
424 /// # Note
425 /// Ensure the `epic` and `size` values provided are valid and match required market conditions.
426 #[must_use]
427 pub fn buy_option_to_market(
428 epic: String,
429 size: f64,
430 expiry: Option<String>,
431 deal_reference: Option<String>,
432 currency_code: Option<String>,
433 ) -> Self {
434 Self::option_to_market(
435 Direction::Buy,
436 true,
437 epic,
438 size,
439 expiry,
440 deal_reference,
441 currency_code,
442 )
443 }
444
445 /// Constructs a new instance of an order to buy an option in the market with optional force_open behavior.
446 ///
447 /// # Parameters
448 ///
449 /// * `epic` - A `String` representing the unique identifier of the instrument to be traded.
450 /// * `size` - A `f64` value that represents the size of the order.
451 /// * `expiry` - An optional `String` representing the expiry date of the option.
452 /// * `deal_reference` - An optional `String` for the deal reference identifier.
453 /// * `currency_code` - An optional `String` representing the currency in which the order is denominated.
454 /// Defaults to "EUR" if not provided.
455 /// * `force_open` - A `bool` indicating whether to force open a new position regardless of existing positions.
456 ///
457 /// # Returns
458 ///
459 /// Returns a new instance of `Self`, representing the constructed order with the provided parameters.
460 ///
461 /// # Behavior
462 ///
463 /// * The size of the order will be rounded to the nearest two decimal places for precision.
464 /// * If a `currency_code` is not provided, the default currency code "EUR" is used.
465 /// * Other parameters are directly mapped into the returned instance.
466 ///
467 /// # Notes
468 ///
469 /// * This function assumes that other order-related fields such as `level`, `stop_level`, `stop_distance`,
470 /// etc., are set to their defaults or require specific business logic, such as
471 /// `DEFAULT_ORDER_BUY_SIZE` for the initial buy size.
472 #[must_use]
473 pub fn buy_option_to_market_w_force(
474 epic: String,
475 size: f64,
476 expiry: Option<String>,
477 deal_reference: Option<String>,
478 currency_code: Option<String>,
479 force_open: bool,
480 ) -> Self {
481 Self::option_to_market(
482 Direction::Buy,
483 force_open,
484 epic,
485 size,
486 expiry,
487 deal_reference,
488 currency_code,
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)]
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}
931
932// ============================================================================
933// WATCHLIST REQUESTS
934// ============================================================================
935
936/// Request to create a new watchlist
937#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
938pub struct CreateWatchlistRequest {
939 /// Name for the new watchlist
940 pub name: String,
941 /// Optional list of EPICs to add to the watchlist
942 #[serde(skip_serializing_if = "Option::is_none")]
943 pub epics: Option<Vec<String>>,
944}
945
946impl CreateWatchlistRequest {
947 /// Create a new watchlist request with just a name
948 #[must_use]
949 pub fn new(name: &str) -> Self {
950 Self {
951 name: name.to_string(),
952 epics: None,
953 }
954 }
955
956 /// Create a watchlist request with name and epics
957 #[must_use]
958 pub fn with_epics(name: &str, epics: Vec<String>) -> Self {
959 Self {
960 name: name.to_string(),
961 epics: Some(epics),
962 }
963 }
964}
965
966/// Request to add an instrument to a watchlist
967#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
968pub struct AddToWatchlistRequest {
969 /// EPIC of the instrument to add
970 pub epic: String,
971}
972
973// ============================================================================
974// WORKING ORDER UPDATE REQUEST
975// ============================================================================
976
977/// Request to update an existing working order
978#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
979pub struct UpdateWorkingOrderRequest {
980 /// Good till date for the order (format: yyyy/MM/dd HH:mm:ss)
981 #[serde(rename = "goodTillDate", skip_serializing_if = "Option::is_none")]
982 pub good_till_date: Option<String>,
983 /// Order level
984 pub level: f64,
985 /// Distance from current price to limit level
986 #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
987 pub limit_distance: Option<f64>,
988 /// Limit level
989 #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
990 pub limit_level: Option<f64>,
991 /// Distance from current price to stop level
992 #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
993 pub stop_distance: Option<f64>,
994 /// Stop level
995 #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
996 pub stop_level: Option<f64>,
997 /// Whether the stop is guaranteed
998 #[serde(rename = "guaranteedStop")]
999 pub guaranteed_stop: bool,
1000 /// Time in force
1001 #[serde(rename = "timeInForce")]
1002 pub time_in_force: TimeInForce,
1003 /// Order type (LIMIT or STOP)
1004 #[serde(rename = "type")]
1005 pub order_type: OrderType,
1006}
1007
1008impl UpdateWorkingOrderRequest {
1009 /// Create a new update working order request
1010 #[must_use]
1011 pub fn new(level: f64, order_type: OrderType, time_in_force: TimeInForce) -> Self {
1012 Self {
1013 level,
1014 order_type,
1015 time_in_force,
1016 guaranteed_stop: false,
1017 good_till_date: None,
1018 limit_distance: None,
1019 limit_level: None,
1020 stop_distance: None,
1021 stop_level: None,
1022 }
1023 }
1024
1025 /// Set the stop level
1026 #[must_use]
1027 pub fn with_stop_level(mut self, stop_level: f64) -> Self {
1028 self.stop_level = Some(stop_level);
1029 self
1030 }
1031
1032 /// Set the limit level
1033 #[must_use]
1034 pub fn with_limit_level(mut self, limit_level: f64) -> Self {
1035 self.limit_level = Some(limit_level);
1036 self
1037 }
1038
1039 /// Set guaranteed stop
1040 #[must_use]
1041 pub fn with_guaranteed_stop(mut self, guaranteed: bool) -> Self {
1042 self.guaranteed_stop = guaranteed;
1043 self
1044 }
1045}
1046
1047// ============================================================================
1048// INDICATIVE COSTS REQUESTS
1049// ============================================================================
1050
1051/// Request for indicative costs when opening a position
1052#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
1053pub struct OpenCostsRequest {
1054 /// Instrument epic
1055 pub epic: String,
1056 /// Trade direction (BUY or SELL)
1057 pub direction: Direction,
1058 /// Trade size
1059 pub size: f64,
1060 /// Order type
1061 #[serde(rename = "orderType")]
1062 pub order_type: OrderType,
1063 /// Currency code
1064 #[serde(rename = "currencyCode")]
1065 pub currency_code: String,
1066 /// Whether the stop is guaranteed
1067 #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
1068 pub guaranteed_stop: Option<bool>,
1069 /// Stop distance
1070 #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
1071 pub stop_distance: Option<f64>,
1072 /// Limit distance
1073 #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
1074 pub limit_distance: Option<f64>,
1075}
1076
1077impl OpenCostsRequest {
1078 /// Create a new open costs request
1079 #[must_use]
1080 pub fn new(epic: &str, direction: Direction, size: f64, currency_code: &str) -> Self {
1081 Self {
1082 epic: epic.to_string(),
1083 direction,
1084 size,
1085 order_type: OrderType::Market,
1086 currency_code: currency_code.to_string(),
1087 guaranteed_stop: None,
1088 stop_distance: None,
1089 limit_distance: None,
1090 }
1091 }
1092}
1093
1094/// Request for indicative costs when closing a position
1095#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
1096pub struct CloseCostsRequest {
1097 /// Deal ID of the position to close
1098 #[serde(rename = "dealId")]
1099 pub deal_id: String,
1100 /// Trade direction (opposite of the position direction)
1101 pub direction: Direction,
1102 /// Size to close
1103 pub size: f64,
1104 /// Order type
1105 #[serde(rename = "orderType")]
1106 pub order_type: OrderType,
1107}
1108
1109impl CloseCostsRequest {
1110 /// Create a new close costs request
1111 #[must_use]
1112 pub fn new(deal_id: &str, direction: Direction, size: f64) -> Self {
1113 Self {
1114 deal_id: deal_id.to_string(),
1115 direction,
1116 size,
1117 order_type: OrderType::Market,
1118 }
1119 }
1120}
1121
1122/// Request for indicative costs when editing a position
1123#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize)]
1124pub struct EditCostsRequest {
1125 /// Deal ID of the position to edit
1126 #[serde(rename = "dealId")]
1127 pub deal_id: String,
1128 /// New stop level
1129 #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
1130 pub stop_level: Option<f64>,
1131 /// New limit level
1132 #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
1133 pub limit_level: Option<f64>,
1134 /// Whether the stop is guaranteed
1135 #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
1136 pub guaranteed_stop: Option<bool>,
1137}
1138
1139impl EditCostsRequest {
1140 /// Create a new edit costs request
1141 #[must_use]
1142 pub fn new(deal_id: &str) -> Self {
1143 Self {
1144 deal_id: deal_id.to_string(),
1145 stop_level: None,
1146 limit_level: None,
1147 guaranteed_stop: None,
1148 }
1149 }
1150
1151 /// Set the new stop level
1152 #[must_use]
1153 pub fn with_stop_level(mut self, stop_level: f64) -> Self {
1154 self.stop_level = Some(stop_level);
1155 self
1156 }
1157
1158 /// Set the new limit level
1159 #[must_use]
1160 pub fn with_limit_level(mut self, limit_level: f64) -> Self {
1161 self.limit_level = Some(limit_level);
1162 self
1163 }
1164}
1165
1166// ============================================================================
1167// ACCOUNT PREFERENCES REQUEST
1168// ============================================================================
1169
1170/// Request to update account preferences
1171#[derive(DebugPretty, DisplaySimple, Clone, Serialize, Deserialize, Default)]
1172pub struct UpdatePreferencesRequest {
1173 /// Whether trailing stops should be enabled
1174 #[serde(rename = "trailingStopsEnabled")]
1175 pub trailing_stops_enabled: bool,
1176}