1use 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#[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}