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
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/// Helper function to deserialize a nullable status field
215/// When the status is null in the JSON, we default to Rejected status
216fn deserialize_nullable_status<'de, D>(deserializer: D) -> Result<Status, D::Error>
217where
218    D: Deserializer<'de>,
219{
220    let opt = Option::deserialize(deserializer)?;
221    Ok(opt.unwrap_or(Status::Rejected))
222}
223
224/// Details of a confirmed order
225#[derive(Debug, Clone, Deserialize)]
226pub struct OrderConfirmation {
227    /// Date and time of the confirmation
228    pub date: String,
229    /// Status of the order (accepted, rejected, etc.)
230    /// This can be null in some responses (e.g., when market is closed)
231    #[serde(deserialize_with = "deserialize_nullable_status")]
232    pub status: Status,
233    /// Reason for rejection if applicable
234    pub reason: Option<String>,
235    /// Unique identifier for the deal
236    #[serde(rename = "dealId")]
237    pub deal_id: Option<String>,
238    /// Client-generated reference for the deal
239    #[serde(rename = "dealReference")]
240    pub deal_reference: String,
241    /// Status of the deal
242    #[serde(rename = "dealStatus")]
243    pub deal_status: Option<String>,
244    /// Instrument EPIC identifier
245    pub epic: Option<String>,
246    /// Expiry date for the order
247    #[serde(rename = "expiry")]
248    pub expiry: Option<String>,
249    /// Whether a guaranteed stop was used
250    #[serde(rename = "guaranteedStop")]
251    pub guaranteed_stop: Option<bool>,
252    /// Price level of the order
253    #[serde(rename = "level")]
254    pub level: Option<f64>,
255    /// Distance for take profit
256    #[serde(rename = "limitDistance")]
257    pub limit_distance: Option<f64>,
258    /// Price level for take profit
259    #[serde(rename = "limitLevel")]
260    pub limit_level: Option<f64>,
261    /// Size/quantity of the order
262    pub size: Option<f64>,
263    /// Distance for stop loss
264    #[serde(rename = "stopDistance")]
265    pub stop_distance: Option<f64>,
266    /// Price level for stop loss
267    #[serde(rename = "stopLevel")]
268    pub stop_level: Option<f64>,
269    /// Whether a trailing stop was used
270    #[serde(rename = "trailingStop")]
271    pub trailing_stop: Option<bool>,
272    /// Direction of the order (buy or sell)
273    pub direction: Option<Direction>,
274}
275
276/// Model for updating an existing position
277#[derive(Debug, Clone, Serialize)]
278pub struct UpdatePositionRequest {
279    /// New price level for stop loss
280    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
281    pub stop_level: Option<f64>,
282    /// New price level for take profit
283    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
284    pub limit_level: Option<f64>,
285    /// Whether to enable trailing stop
286    #[serde(rename = "trailingStop", skip_serializing_if = "Option::is_none")]
287    pub trailing_stop: Option<bool>,
288    /// Distance for trailing stop
289    #[serde(
290        rename = "trailingStopDistance",
291        skip_serializing_if = "Option::is_none"
292    )]
293    pub trailing_stop_distance: Option<f64>,
294}
295
296/// Model for closing an existing position
297#[derive(Debug, Clone, Serialize)]
298pub struct ClosePositionRequest {
299    /// Unique identifier for the position to close
300    #[serde(rename = "dealId")]
301    pub deal_id: String,
302    /// Direction of the closing order (opposite to the position)
303    pub direction: Direction,
304    /// Size/quantity to close
305    pub size: f64,
306    /// Type of order to use for closing
307    #[serde(rename = "orderType")]
308    pub order_type: OrderType,
309    /// Order duration for the closing order
310    #[serde(rename = "timeInForce")]
311    pub time_in_force: TimeInForce,
312    /// Price level for limit close orders
313    #[serde(rename = "level", skip_serializing_if = "Option::is_none")]
314    pub level: Option<f64>,
315    /// Whether to force open a new position
316    #[serde(rename = "forceOpen")]
317    pub force_open: bool,
318    /// Expiry date for the order
319    #[serde(rename = "expiry")]
320    pub expiry: String,
321    /// Instrument EPIC identifier
322    pub epic: String,
323    /// Currency code for the order (e.g., "USD", "EUR")
324    #[serde(rename = "currencyCode")]
325    pub currency_code: String,
326    /// Whether to use a guaranteed stop
327    #[serde(rename = "guaranteedStop")]
328    pub guaranteed_stop: bool,
329}
330
331impl ClosePositionRequest {
332    /// Creates a request to close a position at market price
333    pub fn market(deal_id: String, direction: Direction, size: f64, epic: String) -> Self {
334        Self {
335            deal_id,
336            direction,
337            size,
338            order_type: OrderType::Market,
339            time_in_force: TimeInForce::FillOrKill,
340            level: None,
341            force_open: false,
342            expiry: "JUL-25".to_string(), // Use the same expiry as the position we're closing
343            epic,
344            currency_code: "EUR".to_string(), // Use EUR as the default currency code
345            guaranteed_stop: false,           // Do not use a guaranteed stop by default
346        }
347    }
348
349    /// Creates a request to close a position at a specific price level
350    ///
351    /// This is useful for instruments that don't support market orders
352    pub fn limit(
353        deal_id: String,
354        direction: Direction,
355        size: f64,
356        epic: String,
357        level: f64,
358    ) -> Self {
359        Self {
360            deal_id,
361            direction,
362            size,
363            order_type: OrderType::Limit,
364            time_in_force: TimeInForce::FillOrKill,
365            level: Some(level),
366            force_open: false,
367            expiry: "JUL-25".to_string(), // Use the same expiry as the position we're closing
368            epic,
369            currency_code: "EUR".to_string(), // Use EUR as the default currency code
370            guaranteed_stop: false,           // Do not use a guaranteed stop by default
371        }
372    }
373}
374
375/// Response to closing a position
376#[derive(Debug, Clone, Deserialize)]
377pub struct ClosePositionResponse {
378    /// Client-generated reference for the closing deal
379    #[serde(rename = "dealReference")]
380    pub deal_reference: String,
381}
382
383/// Response to updating a position
384#[derive(Debug, Clone, Deserialize)]
385pub struct UpdatePositionResponse {
386    /// Client-generated reference for the update deal
387    #[serde(rename = "dealReference")]
388    pub deal_reference: String,
389}
390
391/// Model for creating a new working order
392#[derive(Debug, Clone, Serialize)]
393pub struct CreateWorkingOrderRequest {
394    /// Instrument EPIC identifier
395    pub epic: String,
396    /// Order direction (buy or sell)
397    pub direction: Direction,
398    /// Order size/quantity
399    pub size: f64,
400    /// Price level for the order
401    pub level: f64,
402    /// Type of working order (LIMIT or STOP)
403    #[serde(rename = "type")]
404    pub order_type: OrderType,
405    /// Order duration (how long the order remains valid)
406    #[serde(rename = "timeInForce")]
407    pub time_in_force: TimeInForce,
408    /// Whether to use a guaranteed stop
409    #[serde(rename = "guaranteedStop", skip_serializing_if = "Option::is_none")]
410    pub guaranteed_stop: Option<bool>,
411    /// Price level for stop loss
412    #[serde(rename = "stopLevel", skip_serializing_if = "Option::is_none")]
413    pub stop_level: Option<f64>,
414    /// Distance for stop loss
415    #[serde(rename = "stopDistance", skip_serializing_if = "Option::is_none")]
416    pub stop_distance: Option<f64>,
417    /// Price level for take profit
418    #[serde(rename = "limitLevel", skip_serializing_if = "Option::is_none")]
419    pub limit_level: Option<f64>,
420    /// Distance for take profit
421    #[serde(rename = "limitDistance", skip_serializing_if = "Option::is_none")]
422    pub limit_distance: Option<f64>,
423    /// Expiry date for GTD orders
424    #[serde(rename = "goodTillDate", skip_serializing_if = "Option::is_none")]
425    pub good_till_date: Option<String>,
426    /// Client-generated reference for the deal
427    #[serde(rename = "dealReference", skip_serializing_if = "Option::is_none")]
428    pub deal_reference: Option<String>,
429    /// Currency code for the order (e.g., "USD", "EUR")
430    #[serde(rename = "currencyCode", skip_serializing_if = "Option::is_none")]
431    pub currency_code: Option<String>,
432}
433
434impl CreateWorkingOrderRequest {
435    /// Creates a new limit working order
436    pub fn limit(epic: String, direction: Direction, size: f64, level: f64) -> Self {
437        Self {
438            epic,
439            direction,
440            size,
441            level,
442            order_type: OrderType::Limit,
443            time_in_force: TimeInForce::GoodTillCancelled,
444            guaranteed_stop: None,
445            stop_level: None,
446            stop_distance: None,
447            limit_level: None,
448            limit_distance: None,
449            good_till_date: None,
450            deal_reference: None,
451            currency_code: None,
452        }
453    }
454
455    /// Creates a new stop working order
456    pub fn stop(epic: String, direction: Direction, size: f64, level: f64) -> Self {
457        Self {
458            epic,
459            direction,
460            size,
461            level,
462            order_type: OrderType::Stop,
463            time_in_force: TimeInForce::GoodTillCancelled,
464            guaranteed_stop: None,
465            stop_level: None,
466            stop_distance: None,
467            limit_level: None,
468            limit_distance: None,
469            good_till_date: None,
470            deal_reference: None,
471            currency_code: None,
472        }
473    }
474
475    /// Adds a stop loss to the working order
476    pub fn with_stop_loss(mut self, stop_level: f64) -> Self {
477        self.stop_level = Some(stop_level);
478        self
479    }
480
481    /// Adds a take profit to the working order
482    pub fn with_take_profit(mut self, limit_level: f64) -> Self {
483        self.limit_level = Some(limit_level);
484        self
485    }
486
487    /// Adds a reference to the working order
488    pub fn with_reference(mut self, reference: String) -> Self {
489        self.deal_reference = Some(reference);
490        self
491    }
492
493    /// Sets the order to expire at a specific date
494    pub fn expires_at(mut self, date: String) -> Self {
495        self.time_in_force = TimeInForce::GoodTillDate;
496        self.good_till_date = Some(date);
497        self
498    }
499}
500
501/// Response to working order creation
502#[derive(Debug, Clone, Deserialize)]
503pub struct CreateWorkingOrderResponse {
504    /// Client-generated reference for the deal
505    #[serde(rename = "dealReference")]
506    pub deal_reference: String,
507}