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