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, Serialize};
8
9/// Order direction (buy or sell)
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
11#[serde(rename_all = "UPPERCASE")]
12pub enum Direction {
13    /// Buy direction (long position)
14    #[default]
15    Buy,
16    /// Sell direction (short position)
17    Sell,
18}
19
20impl_json_display!(Direction);
21
22/// Order type
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
24#[serde(rename_all = "UPPERCASE")]
25pub enum OrderType {
26    /// Limit order - executed when price reaches specified level
27    #[default]
28    Limit,
29    /// Market order - executed immediately at current market price
30    Market,
31    /// Quote order - executed at quoted price
32    Quote,
33    /// Stop order - becomes market order when price reaches specified level
34    Stop,
35    /// Stop limit order - becomes limit order when price reaches specified level
36    StopLimit,
37}
38
39/// Represents the status of an order or transaction in the system.
40///
41/// This enum covers various states an order can be in throughout its lifecycle,
42/// from creation to completion or cancellation.
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
44#[serde(rename_all = "UPPERCASE")]
45pub enum Status {
46    /// Order has been amended or modified after initial creation
47    Amended,
48    /// Order has been deleted from the system
49    Deleted,
50    /// Order has been completely closed with all positions resolved
51    #[serde(rename = "FULLY_CLOSED")]
52    FullyClosed,
53    /// Order has been opened and is active in the market
54    Opened,
55    /// Order has been partially closed with some positions still open
56    #[serde(rename = "PARTIALLY_CLOSED")]
57    PartiallyClosed,
58    /// Order has been closed but may differ from FullyClosed in context
59    Closed,
60    /// Default state - order is open and active in the market
61    #[default]
62    Open,
63    /// Order has been updated with new parameters
64    Updated,
65    /// Order has been accepted by the system or exchange
66    Accepted,
67    /// Order has been rejected by the system or exchange
68    Rejected,
69    /// Order is currently working (waiting to be filled)
70    Working,
71    /// Order has been filled (executed)
72    Filled,
73    /// Order has been cancelled
74    Cancelled,
75    /// Order has expired (time in force elapsed)
76    Expired,
77}
78
79/// Order duration (time in force)
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
81pub enum TimeInForce {
82    /// Order remains valid until cancelled by the client
83    #[serde(rename = "GOOD_TILL_CANCELLED")]
84    #[default]
85    GoodTillCancelled,
86    /// Order remains valid until a specified date
87    #[serde(rename = "GOOD_TILL_DATE")]
88    GoodTillDate,
89    /// Order is executed immediately (partially or completely) or cancelled
90    #[serde(rename = "IMMEDIATE_OR_CANCEL")]
91    ImmediateOrCancel,
92    /// Order must be filled completely immediately or cancelled
93    #[serde(rename = "FILL_OR_KILL")]
94    FillOrKill,
95}
96
97/// Model for creating a new order
98#[derive(Debug, Clone, Serialize)]
99pub struct CreateOrderRequest {
100    /// Instrument EPIC identifier
101    pub epic: String,
102    /// Order direction (buy or sell)
103    pub direction: Direction,
104    /// Order size/quantity
105    pub size: f64,
106    /// Type of order (market, limit, etc.)
107    #[serde(rename = "orderType")]
108    pub order_type: OrderType,
109    /// Order duration (how long the order remains valid)
110    #[serde(rename = "timeInForce")]
111    pub time_in_force: TimeInForce,
112    /// Price level for limit orders
113    #[serde(rename = "level", skip_serializing_if = "Option::is_none")]
114    pub level: Option<f64>,
115    /// Whether to use a guaranteed stop
116    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
117    pub guaranteed_stop: Option<bool>,
118    /// Price level for stop loss
119    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
120    pub stop_level: Option<f64>,
121    /// Distance for stop loss
122    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
123    pub stop_distance: Option<f64>,
124    /// Price level for take profit
125    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
126    pub limit_level: Option<f64>,
127    /// Distance for take profit
128    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
129    pub limit_distance: Option<f64>,
130    /// Expiry date for the order
131    #[serde(rename = "expiry", skip_serializing_if = "Option::is_none")]
132    pub expiry: Option<String>,
133    /// Client-generated reference for the deal
134    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
135    pub deal_reference: Option<String>,
136    /// Whether to force open a new position
137    #[serde(rename = "forceOpen", skip_serializing_if = "Option::is_none")]
138    pub force_open: Option<bool>,
139    /// Currency code for the order (e.g., "USD", "EUR")
140    #[serde(rename = "currencyCode", skip_serializing_if = "Option::is_none")]
141    pub currency_code: Option<String>,
142}
143
144impl CreateOrderRequest {
145    /// Creates a new market order
146    pub fn market(epic: String, direction: Direction, size: f64) -> Self {
147        Self {
148            epic,
149            direction,
150            size,
151            order_type: OrderType::Market,
152            time_in_force: TimeInForce::FillOrKill,
153            level: None,
154            guaranteed_stop: None,
155            stop_level: None,
156            stop_distance: None,
157            limit_level: None,
158            limit_distance: None,
159            expiry: None,
160            deal_reference: None,
161            force_open: Some(true),
162            currency_code: None,
163        }
164    }
165
166    /// Creates a new limit order
167    pub fn limit(epic: String, direction: Direction, size: f64, level: f64) -> Self {
168        Self {
169            epic,
170            direction,
171            size,
172            order_type: OrderType::Limit,
173            time_in_force: TimeInForce::GoodTillCancelled,
174            level: Some(level),
175            guaranteed_stop: None,
176            stop_level: None,
177            stop_distance: None,
178            limit_level: None,
179            limit_distance: None,
180            expiry: None,
181            deal_reference: None,
182            force_open: Some(true),
183            currency_code: None,
184        }
185    }
186
187    /// Adds a stop loss to the order
188    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
189        self.stop_level = Some(stop_level);
190        self
191    }
192
193    /// Adds a take profit to the order
194    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
195        self.limit_level = Some(limit_level);
196        self
197    }
198
199    /// Adds a reference to the order
200    pub fn with_reference(mut self, reference: String) -> Self {
201        self.deal_reference = Some(reference);
202        self
203    }
204}
205
206/// Response to order creation
207#[derive(Debug, Clone, Deserialize)]
208pub struct CreateOrderResponse {
209    /// Client-generated reference for the deal
210    #[serde(rename = "dealReference")]
211    pub deal_reference: String,
212}
213
214/// Details of a confirmed order
215#[derive(Debug, Clone, Deserialize)]
216pub struct OrderConfirmation {
217    /// Date and time of the confirmation
218    pub date: String,
219    /// Status of the order (accepted, rejected, etc.)
220    pub status: Status,
221    /// Reason for rejection if applicable
222    pub reason: Option<String>,
223    /// Unique identifier for the deal
224    #[serde(rename = "dealId")]
225    pub deal_id: Option<String>,
226    /// Client-generated reference for the deal
227    #[serde(rename = "dealReference")]
228    pub deal_reference: String,
229    /// Status of the deal
230    #[serde(rename = "dealStatus")]
231    pub deal_status: Option<String>,
232    /// Instrument EPIC identifier
233    pub epic: Option<String>,
234    /// Expiry date for the order
235    #[serde(rename = "expiry")]
236    pub expiry: Option<String>,
237    /// Whether a guaranteed stop was used
238    #[serde(rename = "guaranteedStop")]
239    pub guaranteed_stop: Option<bool>,
240    /// Price level of the order
241    #[serde(rename = "level")]
242    pub level: Option<f64>,
243    /// Distance for take profit
244    #[serde(rename = "limitDistance")]
245    pub limit_distance: Option<f64>,
246    /// Price level for take profit
247    #[serde(rename = "limitLevel")]
248    pub limit_level: Option<f64>,
249    /// Size/quantity of the order
250    pub size: Option<f64>,
251    /// Distance for stop loss
252    #[serde(rename = "stopDistance")]
253    pub stop_distance: Option<f64>,
254    /// Price level for stop loss
255    #[serde(rename = "stopLevel")]
256    pub stop_level: Option<f64>,
257    /// Whether a trailing stop was used
258    #[serde(rename = "trailingStop")]
259    pub trailing_stop: Option<bool>,
260    /// Direction of the order (buy or sell)
261    pub direction: Option<Direction>,
262}
263
264/// Model for updating an existing position
265#[derive(Debug, Clone, Serialize)]
266pub struct UpdatePositionRequest {
267    /// New price level for stop loss
268    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
269    pub stop_level: Option<f64>,
270    /// New price level for take profit
271    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
272    pub limit_level: Option<f64>,
273    /// Whether to enable trailing stop
274    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
275    pub trailing_stop: Option<bool>,
276    /// Distance for trailing stop
277    #[serde(
278        rename = "trailingStopDistance",
279        skip_serializing_if = "Option::is_none"
280    )]
281    pub trailing_stop_distance: Option<f64>,
282}
283
284/// Model for closing an existing position
285#[derive(Debug, Clone, Serialize)]
286pub struct ClosePositionRequest {
287    /// Unique identifier for the position to close
288    #[serde(rename = "dealId")]
289    pub deal_id: String,
290    /// Direction of the closing order (opposite to the position)
291    pub direction: Direction,
292    /// Size/quantity to close
293    pub size: f64,
294    /// Type of order to use for closing
295    #[serde(rename = "orderType")]
296    pub order_type: OrderType,
297    /// Order duration for the closing order
298    #[serde(rename = "timeInForce")]
299    pub time_in_force: TimeInForce,
300    /// Price level for limit close orders
301    #[serde(rename = "level", skip_serializing_if = "Option::is_none")]
302    pub level: Option<f64>,
303    /// Whether to force open a new position
304    #[serde(rename = "forceOpen")]
305    pub force_open: bool,
306    /// Expiry date for the order
307    #[serde(rename = "expiry")]
308    pub expiry: String,
309    /// Instrument EPIC identifier
310    pub epic: String,
311    /// Currency code for the order (e.g., "USD", "EUR")
312    #[serde(rename = "currencyCode")]
313    pub currency_code: String,
314    /// Whether to use a guaranteed stop
315    #[serde(rename = "guaranteedStop")]
316    pub guaranteed_stop: bool,
317}
318
319impl ClosePositionRequest {
320    /// Creates a request to close a position at market price
321    pub fn market(deal_id: String, direction: Direction, size: f64, epic: String) -> Self {
322        Self {
323            deal_id,
324            direction,
325            size,
326            order_type: OrderType::Market,
327            time_in_force: TimeInForce::FillOrKill,
328            level: None,
329            force_open: false,
330            expiry: "JUL-25".to_string(), // Use the same expiry as the position we're closing
331            epic,
332            currency_code: "EUR".to_string(), // Use EUR as the default currency code
333            guaranteed_stop: false,           // Do not use a guaranteed stop by default
334        }
335    }
336
337    /// Creates a request to close a position at a specific price level
338    ///
339    /// This is useful for instruments that don't support market orders
340    pub fn limit(
341        deal_id: String,
342        direction: Direction,
343        size: f64,
344        epic: String,
345        level: f64,
346    ) -> Self {
347        Self {
348            deal_id,
349            direction,
350            size,
351            order_type: OrderType::Limit,
352            time_in_force: TimeInForce::FillOrKill,
353            level: Some(level),
354            force_open: false,
355            expiry: "JUL-25".to_string(), // Use the same expiry as the position we're closing
356            epic,
357            currency_code: "EUR".to_string(), // Use EUR as the default currency code
358            guaranteed_stop: false,           // Do not use a guaranteed stop by default
359        }
360    }
361}
362
363/// Response to closing a position
364#[derive(Debug, Clone, Deserialize)]
365pub struct ClosePositionResponse {
366    /// Client-generated reference for the closing deal
367    #[serde(rename = "dealReference")]
368    pub deal_reference: String,
369}
370
371/// Response to updating a position
372#[derive(Debug, Clone, Deserialize)]
373pub struct UpdatePositionResponse {
374    /// Client-generated reference for the update deal
375    #[serde(rename = "dealReference")]
376    pub deal_reference: String,
377}
378
379/// Model for creating a new working order
380#[derive(Debug, Clone, Serialize)]
381pub struct CreateWorkingOrderRequest {
382    /// Instrument EPIC identifier
383    pub epic: String,
384    /// Order direction (buy or sell)
385    pub direction: Direction,
386    /// Order size/quantity
387    pub size: f64,
388    /// Price level for the order
389    pub level: f64,
390    /// Type of working order (LIMIT or STOP)
391    #[serde(rename = "type")]
392    pub order_type: OrderType,
393    /// Order duration (how long the order remains valid)
394    #[serde(rename = "timeInForce")]
395    pub time_in_force: TimeInForce,
396    /// Whether to use a guaranteed stop
397    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
398    pub guaranteed_stop: Option<bool>,
399    /// Price level for stop loss
400    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
401    pub stop_level: Option<f64>,
402    /// Distance for stop loss
403    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
404    pub stop_distance: Option<f64>,
405    /// Price level for take profit
406    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
407    pub limit_level: Option<f64>,
408    /// Distance for take profit
409    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
410    pub limit_distance: Option<f64>,
411    /// Expiry date for GTD orders
412    #[serde(rename = "goodTillDate", skip_serializing_if = "Option::is_none")]
413    pub good_till_date: Option<String>,
414    /// Client-generated reference for the deal
415    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
416    pub deal_reference: Option<String>,
417    /// Currency code for the order (e.g., "USD", "EUR")
418    #[serde(rename = "currencyCode", skip_serializing_if = "Option::is_none")]
419    pub currency_code: Option<String>,
420}
421
422impl CreateWorkingOrderRequest {
423    /// Creates a new limit working order
424    pub fn limit(epic: String, direction: Direction, size: f64, level: f64) -> Self {
425        Self {
426            epic,
427            direction,
428            size,
429            level,
430            order_type: OrderType::Limit,
431            time_in_force: TimeInForce::GoodTillCancelled,
432            guaranteed_stop: None,
433            stop_level: None,
434            stop_distance: None,
435            limit_level: None,
436            limit_distance: None,
437            good_till_date: None,
438            deal_reference: None,
439            currency_code: None,
440        }
441    }
442
443    /// Creates a new stop working order
444    pub fn stop(epic: String, direction: Direction, size: f64, level: f64) -> Self {
445        Self {
446            epic,
447            direction,
448            size,
449            level,
450            order_type: OrderType::Stop,
451            time_in_force: TimeInForce::GoodTillCancelled,
452            guaranteed_stop: None,
453            stop_level: None,
454            stop_distance: None,
455            limit_level: None,
456            limit_distance: None,
457            good_till_date: None,
458            deal_reference: None,
459            currency_code: None,
460        }
461    }
462
463    /// Adds a stop loss to the working order
464    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
465        self.stop_level = Some(stop_level);
466        self
467    }
468
469    /// Adds a take profit to the working order
470    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
471        self.limit_level = Some(limit_level);
472        self
473    }
474
475    /// Adds a reference to the working order
476    pub fn with_reference(mut self, reference: String) -> Self {
477        self.deal_reference = Some(reference);
478        self
479    }
480
481    /// Sets the order to expire at a specific date
482    pub fn expires_at(mut self, date: String) -> Self {
483        self.time_in_force = TimeInForce::GoodTillDate;
484        self.good_till_date = Some(date);
485        self
486    }
487}
488
489/// Response to working order creation
490#[derive(Debug, Clone, Deserialize)]
491pub struct CreateWorkingOrderResponse {
492    /// Client-generated reference for the deal
493    #[serde(rename = "dealReference")]
494    pub deal_reference: String,
495}