Skip to main content

truefix_ig_client/
types.rs

1//! Request and response types for the supported IG REST operations.
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
6pub enum Direction {
7    #[serde(rename = "BUY")]
8    Buy,
9    #[serde(rename = "SELL")]
10    Sell,
11}
12
13#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
14pub enum OrderType {
15    #[serde(rename = "MARKET")]
16    Market,
17    #[serde(rename = "LIMIT")]
18    Limit,
19    #[serde(rename = "STOP")]
20    Stop,
21    #[serde(rename = "QUOTE")]
22    Quote,
23}
24
25#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
26pub enum TimeInForce {
27    #[serde(rename = "GOOD_TILL_CANCELLED")]
28    GoodTillCancelled,
29    #[serde(rename = "GOOD_TILL_DATE")]
30    GoodTillDate,
31}
32
33#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
34pub enum MarketStatus {
35    #[serde(rename = "TRADEABLE")]
36    Tradeable,
37    #[serde(rename = "CLOSED")]
38    Closed,
39    #[serde(rename = "EDITS_ONLY")]
40    EditsOnly,
41    #[serde(rename = "OFFLINE")]
42    Offline,
43    #[serde(other)]
44    Unknown,
45}
46
47#[derive(Debug, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct LoginResponse {
50    pub current_account_id: Option<String>,
51    pub lightstreamer_endpoint: String,
52    pub client_id: String,
53    pub currency_iso_code: Option<String>,
54    pub dealing_enabled: Option<bool>,
55}
56
57#[derive(Debug, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub(crate) struct V3LoginResponse {
60    pub account_id: Option<String>,
61    pub current_account_id: Option<String>,
62    pub lightstreamer_endpoint: String,
63    pub client_id: String,
64    pub currency_iso_code: Option<String>,
65    pub dealing_enabled: Option<bool>,
66    pub oauth_token: Option<OAuthToken>,
67}
68
69#[derive(Debug, Deserialize)]
70pub(crate) struct OAuthToken {
71    #[serde(alias = "accessToken")]
72    pub access_token: String,
73    #[serde(alias = "refreshToken")]
74    pub refresh_token: String,
75    #[serde(
76        alias = "expiresIn",
77        deserialize_with = "deserialize_u64_string_or_number"
78    )]
79    pub expires_in: u64,
80}
81
82fn deserialize_u64_string_or_number<'de, D>(deserializer: D) -> Result<u64, D::Error>
83where
84    D: serde::Deserializer<'de>,
85{
86    #[derive(Deserialize)]
87    #[serde(untagged)]
88    enum Value {
89        Number(u64),
90        String(String),
91    }
92
93    match Value::deserialize(deserializer)? {
94        Value::Number(value) => Ok(value),
95        Value::String(value) => value.parse().map_err(serde::de::Error::custom),
96    }
97}
98
99#[derive(Debug, Deserialize)]
100pub struct AccountsResponse {
101    pub accounts: Vec<Account>,
102}
103
104#[derive(Debug, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct Account {
107    pub account_id: String,
108    pub account_name: String,
109    pub preferred: bool,
110    pub balance: Option<AccountBalance>,
111    pub currency: Option<String>,
112}
113
114#[derive(Debug, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct AccountBalance {
117    pub balance: f64,
118    pub available: f64,
119    pub deposit: f64,
120    pub profit_loss: f64,
121}
122
123#[derive(Debug, Deserialize)]
124pub struct PositionsResponse {
125    pub positions: Vec<Position>,
126}
127
128#[derive(Debug, Deserialize)]
129pub struct Position {
130    pub position: PositionDetail,
131    pub market: PositionMarket,
132}
133
134#[derive(Debug, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct PositionDetail {
137    pub deal_id: String,
138    pub direction: Direction,
139    pub currency: String,
140    pub size: Option<f64>,
141    pub level: Option<f64>,
142}
143
144#[derive(Debug, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct PositionMarket {
147    pub epic: String,
148    pub instrument_name: String,
149    pub bid: Option<f64>,
150    pub offer: Option<f64>,
151    pub market_status: Option<MarketStatus>,
152}
153
154#[derive(Debug, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct MarketDetails {
157    pub instrument: Instrument,
158    pub snapshot: MarketSnapshot,
159    #[serde(default)]
160    pub dealing_rules: Option<DealingRules>,
161}
162
163#[derive(Debug, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct Instrument {
166    pub name: String,
167    pub epic: String,
168    pub instrument_type: Option<String>,
169    pub expiry: Option<String>,
170    #[serde(default)]
171    pub currencies: Vec<InstrumentCurrency>,
172}
173
174#[derive(Debug, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct InstrumentCurrency {
177    pub code: String,
178    #[serde(default)]
179    pub is_default: bool,
180}
181
182#[derive(Debug, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct DealingRules {
185    #[serde(default)]
186    pub min_deal_size: Option<RuleValue>,
187}
188
189#[derive(Debug, Deserialize)]
190pub struct RuleValue {
191    pub value: f64,
192    #[serde(default)]
193    pub unit: Option<String>,
194}
195
196#[derive(Debug, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct MarketSnapshot {
199    pub market_status: MarketStatus,
200    pub bid: Option<f64>,
201    pub offer: Option<f64>,
202    pub high: Option<f64>,
203    pub low: Option<f64>,
204}
205
206#[derive(Debug, Deserialize)]
207pub struct MarketsResponse {
208    pub markets: Vec<MarketData>,
209}
210
211#[derive(Debug, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct MarketData {
214    pub epic: String,
215    pub instrument_name: String,
216    #[serde(default)]
217    pub instrument_type: Option<String>,
218    #[serde(default)]
219    pub expiry: Option<String>,
220    pub bid: Option<f64>,
221    pub offer: Option<f64>,
222}
223
224#[derive(Debug, Clone, Default)]
225pub struct HistoricalPricesQuery<'a> {
226    pub resolution: &'a str,
227    pub from: Option<&'a str>,
228    pub to: Option<&'a str>,
229    pub max: Option<u32>,
230}
231impl<'a> HistoricalPricesQuery<'a> {
232    pub fn new(resolution: &'a str) -> Self {
233        Self {
234            resolution,
235            ..Self::default()
236        }
237    }
238    pub fn from(mut self, value: &'a str) -> Self {
239        self.from = Some(value);
240        self
241    }
242    pub fn to(mut self, value: &'a str) -> Self {
243        self.to = Some(value);
244        self
245    }
246    pub fn max(mut self, value: u32) -> Self {
247        self.max = Some(value);
248        self
249    }
250}
251
252#[derive(Debug, Deserialize)]
253pub struct HistoricalPricesResponse {
254    pub prices: Vec<HistoricalPrice>,
255}
256
257#[derive(Debug, Deserialize)]
258#[serde(rename_all = "camelCase")]
259pub struct HistoricalPrice {
260    pub snapshot_time: Option<String>,
261    pub snapshot_time_utc: Option<String>,
262    pub open_price: PricePoint,
263    pub high_price: PricePoint,
264    pub low_price: PricePoint,
265    pub close_price: PricePoint,
266    #[serde(default)]
267    pub last_traded_volume: Option<f64>,
268}
269
270#[derive(Debug, Default, Deserialize)]
271pub struct PricePoint {
272    pub bid: Option<f64>,
273    pub ask: Option<f64>,
274    pub last_traded: Option<f64>,
275}
276
277#[derive(Debug, Serialize)]
278#[serde(rename_all = "camelCase")]
279pub struct CreatePositionRequest {
280    pub currency_code: String,
281    pub direction: Direction,
282    pub epic: String,
283    pub expiry: String,
284    pub force_open: bool,
285    pub guaranteed_stop: bool,
286    pub order_type: OrderType,
287    pub size: f64,
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub level: Option<f64>,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub limit_level: Option<f64>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub stop_level: Option<f64>,
294}
295
296#[derive(Debug, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct DealReferenceResponse {
299    pub deal_reference: String,
300}
301
302#[derive(Debug, Deserialize)]
303#[serde(rename_all = "camelCase")]
304pub struct SwitchAccountResponse {
305    pub dealing_enabled: bool,
306    #[serde(default)]
307    pub has_active_demo_accounts: bool,
308    #[serde(default)]
309    pub has_active_live_accounts: bool,
310    #[serde(default)]
311    pub trailing_stops_enabled: bool,
312}
313
314#[derive(Debug, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub struct WorkingOrdersResponse {
317    #[serde(default)]
318    pub working_orders: Vec<WorkingOrder>,
319}
320
321#[derive(Debug, Deserialize)]
322#[serde(rename_all = "camelCase")]
323pub struct WorkingOrder {
324    pub working_order_data: WorkingOrderData,
325    pub market_data: MarketData,
326}
327
328#[derive(Debug, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct WorkingOrderData {
331    pub deal_id: String,
332    #[serde(default)]
333    pub deal_reference: Option<String>,
334    pub direction: Direction,
335    pub epic: String,
336    pub order_size: f64,
337    pub order_level: f64,
338    #[serde(rename = "orderType")]
339    pub order_type: OrderType,
340    pub time_in_force: TimeInForce,
341    #[serde(default)]
342    pub good_till_date: Option<String>,
343    #[serde(default)]
344    pub guaranteed_stop: bool,
345    #[serde(default)]
346    pub stop_level: Option<f64>,
347    #[serde(default)]
348    pub stop_distance: Option<f64>,
349    #[serde(default)]
350    pub limit_level: Option<f64>,
351    #[serde(default)]
352    pub limit_distance: Option<f64>,
353    #[serde(default)]
354    pub currency_code: Option<String>,
355}
356
357#[derive(Debug, Serialize)]
358#[serde(rename_all = "camelCase")]
359pub struct CreateWorkingOrderRequest {
360    pub epic: String,
361    pub direction: Direction,
362    pub size: f64,
363    pub level: f64,
364    #[serde(rename = "type")]
365    pub order_type: OrderType,
366    pub time_in_force: TimeInForce,
367    pub guaranteed_stop: bool,
368    #[serde(skip_serializing_if = "Option::is_none")]
369    pub stop_level: Option<f64>,
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub stop_distance: Option<f64>,
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub limit_level: Option<f64>,
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub limit_distance: Option<f64>,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub good_till_date: Option<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub deal_reference: Option<String>,
380    pub currency_code: String,
381    pub expiry: String,
382}
383
384#[derive(Debug, Serialize)]
385#[serde(rename_all = "camelCase")]
386pub struct UpdateWorkingOrderRequest {
387    pub level: f64,
388    #[serde(rename = "type")]
389    pub order_type: OrderType,
390    pub time_in_force: TimeInForce,
391    pub guaranteed_stop: bool,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub good_till_date: Option<String>,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub stop_level: Option<f64>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub stop_distance: Option<f64>,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub limit_level: Option<f64>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub limit_distance: Option<f64>,
402}
403
404/// Authoritative acknowledgement returned by `GET /confirms/{dealReference}`.
405/// Optional fields keep the client forward-compatible with IG's product- and
406/// rejection-specific response shapes.
407#[derive(Debug, Clone, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct DealConfirmation {
410    pub deal_reference: String,
411    #[serde(default)]
412    pub deal_id: Option<String>,
413    pub deal_status: DealStatus,
414    #[serde(default)]
415    pub reason: Option<String>,
416    #[serde(default)]
417    pub status: Option<String>,
418    #[serde(default)]
419    pub level: Option<f64>,
420    #[serde(default)]
421    pub size: Option<f64>,
422}
423
424#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
425#[serde(rename_all = "UPPERCASE")]
426pub enum DealStatus {
427    Accepted,
428    Rejected,
429    #[serde(other)]
430    Unknown,
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn deal_confirmation_accepts_product_specific_optional_fields() {
439        let confirmation: DealConfirmation = serde_json::from_value(serde_json::json!({
440            "dealReference": "reference-1",
441            "dealId": "deal-1",
442            "dealStatus": "ACCEPTED",
443            "status": "OPEN",
444            "level": 123.45,
445            "size": 2,
446            "epic": "CS.D.AAPL.CFD.IP"
447        }))
448        .unwrap();
449        assert_eq!(confirmation.deal_status, DealStatus::Accepted);
450        assert_eq!(confirmation.deal_id.as_deref(), Some("deal-1"));
451    }
452}